| package internal |
|
|
| import ( |
| "context" |
| "errors" |
| "fmt" |
| "time" |
|
|
| "github.com/alpacahq/alpacadecimal" |
| "github.com/invopop/gobl/currency" |
| "github.com/oklog/ulid/v2" |
| "github.com/samber/lo" |
|
|
| "github.com/openmeterio/openmeter/api" |
| "github.com/openmeterio/openmeter/openmeter/billing" |
| billinghttp "github.com/openmeterio/openmeter/openmeter/billing/httpdriver" |
| "github.com/openmeterio/openmeter/openmeter/customer" |
| "github.com/openmeterio/openmeter/openmeter/notification" |
| "github.com/openmeterio/openmeter/openmeter/productcatalog" |
| "github.com/openmeterio/openmeter/pkg/currencyx" |
| "github.com/openmeterio/openmeter/pkg/models" |
| "github.com/openmeterio/openmeter/pkg/timeutil" |
| ) |
|
|
| type TestEventGenerator struct { |
| billingService billing.Service |
| } |
|
|
| func NewTestEventGenerator(billingService billing.Service) *TestEventGenerator { |
| return &TestEventGenerator{ |
| billingService: billingService, |
| } |
| } |
|
|
| type EventGeneratorInput struct { |
| Namespace string |
|
|
| EventType notification.EventType |
| } |
|
|
| func (i EventGeneratorInput) Validate() error { |
| if i.Namespace == "" { |
| return errors.New("namespace is required") |
| } |
|
|
| if i.EventType == "" { |
| return errors.New("event type is required") |
| } |
|
|
| return nil |
| } |
|
|
| func (t *TestEventGenerator) Generate(ctx context.Context, in EventGeneratorInput) (notification.EventPayload, error) { |
| if err := in.Validate(); err != nil { |
| return notification.EventPayload{}, err |
| } |
|
|
| switch in.EventType { |
| case notification.EventTypeBalanceThreshold: |
| return t.newTestBalanceThresholdPayload(), nil |
| case notification.EventTypeEntitlementReset: |
| return t.newTestEntitlementResetPayload(), nil |
| case notification.EventTypeInvoiceCreated, notification.EventTypeInvoiceUpdated: |
| return t.newTestInvoicePayload(ctx, in.Namespace, in.EventType) |
| default: |
| return notification.EventPayload{}, fmt.Errorf("unsupported event type: %s", in.EventType) |
| } |
| } |
|
|
| func (t *TestEventGenerator) newTestBalanceThresholdPayload() notification.EventPayload { |
| payload := t.newTestEntitlementResetPayload() |
| payload.Type = notification.EventTypeBalanceThreshold |
| payload.BalanceThreshold = ¬ification.BalanceThresholdPayload{ |
| EntitlementValuePayloadBase: notification.EntitlementValuePayloadBase(*payload.EntitlementReset), |
| Threshold: api.NotificationRuleBalanceThresholdValue{ |
| Type: api.NotificationRuleBalanceThresholdValueTypePercent, |
| Value: 50, |
| }, |
| } |
| return payload |
| } |
|
|
| func (t *TestEventGenerator) newTestEntitlementResetPayload() notification.EventPayload { |
| var ( |
| now = time.Now() |
| createdAt = now.Add(-24 * time.Hour) |
| updatedAt = now.Add(-12 * time.Hour) |
| from = now.Add(-24 * time.Hour) |
| to = now.Add(-12 * time.Hour) |
| ) |
|
|
| day := &api.RecurringPeriodInterval{} |
|
|
| _ = day.FromRecurringPeriodIntervalEnum(api.RecurringPeriodIntervalEnumDAY) |
|
|
| return notification.EventPayload{ |
| EventPayloadMeta: notification.EventPayloadMeta{ |
| Type: notification.EventTypeBalanceThreshold, |
| }, |
| EntitlementReset: ¬ification.EntitlementResetPayload{ |
| Entitlement: api.EntitlementMetered{ |
| CreatedAt: createdAt, |
| CurrentUsagePeriod: api.Period{ |
| From: from, |
| To: to, |
| }, |
| DeletedAt: nil, |
| FeatureId: "01J5AVN2T6S0RDGJHVNN0BW3F5", |
| FeatureKey: "test-feature-1", |
| Id: "01J5AVNM7H1PT65RDFWGXXPT47", |
| IsSoftLimit: lo.ToPtr(false), |
| IsUnlimited: lo.ToPtr(true), |
| IssueAfterReset: lo.ToPtr(10.0), |
| IssueAfterResetPriority: lo.ToPtr[uint8](5), |
| LastReset: time.Time{}, |
| MeasureUsageFrom: time.Time{}, |
| Metadata: &map[string]string{ |
| "test-metadata-key": "test-metadata-value", |
| }, |
| PreserveOverageAtReset: lo.ToPtr(true), |
| SubjectKey: "test-subject-1", |
| Type: api.EntitlementMeteredTypeMetered, |
| UpdatedAt: updatedAt, |
| UsagePeriod: api.RecurringPeriod{ |
| Anchor: from, |
| Interval: *day, |
| IntervalISO: "P1D", |
| }, |
| }, |
| Feature: api.Feature{ |
| ArchivedAt: nil, |
| CreatedAt: createdAt, |
| DeletedAt: nil, |
| Id: "01J5AVN2T6S0RDGJHVNN0BW3F5", |
| Key: "test-feature-1", |
| Metadata: &map[string]string{ |
| "test-metadata-key": "test-metadata-value", |
| }, |
| MeterGroupByFilters: nil, |
| MeterSlug: lo.ToPtr("test-meter-1"), |
| Name: "test-meter-1", |
| UpdatedAt: updatedAt, |
| }, |
| Subject: api.Subject{ |
| CurrentPeriodEnd: lo.ToPtr(from), |
| CurrentPeriodStart: lo.ToPtr(to), |
| DisplayName: lo.ToPtr("Test Subject 1"), |
| Id: "01J5AW0ZD6T8624PCK0Q5TYX71", |
| Key: "test-subject-1", |
| Metadata: &map[string]interface{}{ |
| "test-metadata-key": "test-metadata-value", |
| }, |
| StripeCustomerId: lo.ToPtr("01J5AW2XS6DYHH7E9PNJSQJ341"), |
| }, |
| Value: api.EntitlementValue{ |
| Balance: lo.ToPtr(10_000.0), |
| HasAccess: true, |
| Overage: lo.ToPtr(99.0), |
| Usage: lo.ToPtr(5_001.0), |
| }, |
| Customer: api.Customer{ |
| BillingAddress: nil, |
| CreatedAt: createdAt, |
| Currency: nil, |
| CurrentSubscriptionId: nil, |
| DeletedAt: nil, |
| Description: nil, |
| Id: "01J5AW2XS6DYHH7E9PNJSQJ341", |
| Key: lo.ToPtr("test-customer-1"), |
| Metadata: nil, |
| Name: "Test Customer 1", |
| PrimaryEmail: nil, |
| Subscriptions: nil, |
| UpdatedAt: updatedAt, |
| UsageAttribution: &api.CustomerUsageAttribution{ |
| SubjectKeys: []string{"test-subject-1"}, |
| }, |
| }, |
| }, |
| } |
| } |
|
|
| func (t *TestEventGenerator) newTestInvoicePayload(ctx context.Context, namespace string, eventType notification.EventType) (notification.EventPayload, error) { |
| now := time.Now().Truncate(time.Second).In(time.UTC) |
|
|
| invoice, err := t.billingService.SimulateInvoice(ctx, billing.SimulateInvoiceInput{ |
| Namespace: namespace, |
| Customer: &customer.Customer{ |
| ManagedResource: models.NewManagedResource(models.ManagedResourceInput{ |
| Namespace: namespace, |
| ID: ulid.Make().String(), |
| Name: "Test Customer", |
| CreatedAt: now, |
| UpdatedAt: now, |
| }), |
| Key: lo.ToPtr("test-customer-1"), |
| UsageAttribution: &customer.CustomerUsageAttribution{ |
| SubjectKeys: []string{"test-subject-1"}, |
| }, |
| PrimaryEmail: lo.ToPtr("test-customer-1@example.com"), |
| Currency: lo.ToPtr(currencyx.Code(currency.USD)), |
| }, |
|
|
| Number: lo.ToPtr("TEST-INV-1"), |
| Currency: currencyx.Code(currency.USD), |
| Lines: billing.NewStandardInvoiceLines([]*billing.StandardLine{ |
| billing.NewFlatFeeLine(billing.NewFlatFeeLineInput{ |
| Namespace: namespace, |
| ID: ulid.Make().String(), |
| CreatedAt: now, |
| UpdatedAt: now, |
|
|
| ManagedBy: billing.ManuallyManagedLine, |
|
|
| Name: "test flat fee", |
| Period: timeutil.ClosedPeriod{ |
| From: now.Add(-time.Hour * 24 * 30), |
| To: now, |
| }, |
| InvoiceAt: now, |
|
|
| PerUnitAmount: alpacadecimal.NewFromInt(1000), |
| PaymentTerm: productcatalog.InAdvancePaymentTerm, |
| }), |
| }), |
| }) |
| if err != nil { |
| return notification.EventPayload{}, err |
| } |
|
|
| eventInvoice, err := billing.NewEventStandardInvoice(invoice) |
| if err != nil { |
| return notification.EventPayload{}, err |
| } |
|
|
| apiInvoice, err := billinghttp.MapEventInvoiceToAPI(eventInvoice) |
| if err != nil { |
| return notification.EventPayload{}, err |
| } |
|
|
| return notification.EventPayload{ |
| EventPayloadMeta: notification.EventPayloadMeta{ |
| Type: eventType, |
| Version: notification.EventPayloadVersionCurrent, |
| }, |
| Invoice: ¬ification.InvoicePayload{Invoice: apiInvoice}, |
| }, nil |
| } |
|
|