subscriptionsync

package
v1.0.0-beta.236 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 9, 2026 License: Apache-2.0 Imports: 7 Imported by: 0

README

Subscription sync

Subscription sync is the reconciliation boundary between the desired schedule owned by Subscription and the billing-side artifacts owned by Billing and Charges.

It translates a subscription view into invoice lines or charge intents. It does not edit subscriptions, calculate invoice totals, realize charges, or advance invoice and payment lifecycles.

Reconciliation model

A sync is given a subscription reference or expanded view and an asOf horizon. It resolves the subscription, including deleted records, and the customer's billing profile before acquiring billing's customer lock. Under that lock it:

  1. loads the persisted invoice-line and charge state owned by that subscription
  2. builds the target billable periods from the resolved subscription view
  3. diffs target and persisted items by their subscription-sync identity
  4. applies invoice patches, then charge patches
  5. records whether the subscription still has billables and when it next needs reconciliation

This is reconciliation, not event handling by side effect. Repeating the same sync must converge. Subscription events provide the prompt path, invoice creation triggers a refill of consumed future work, and the periodic reconciler repairs missed or failed event processing.

Invoice patches, charge patches, and the sync-state update share the billing transaction. A failure rolls back the plan rather than leaving only the earlier patches applied.

The entrypoints that also invoice the customer run pending-line collection after reconciliation so flat fees billed in advance can become invoices immediately.

Target time model

The target builder walks each phase and item version into service periods up to the requested horizon. The horizon normally expands to the end of the aligned billing period containing asOf; it is not simply a database query cutoff.

Each target item keeps three periods:

  • service period: the portion of service represented by this artifact
  • full service period: the untruncated period used for proration
  • billing period: the subscription-aligned outer period

Flat fees billed in advance use the billing-period start as InvoiceAt. Everything else is invoiced no earlier than both the service-period and billing-period end.

Recurring artifacts use the stable identity:

{subscriptionID}/{phaseKey}/{itemKey}/v[{itemVersion}]/period[{periodIndex}]

One-time artifacts omit the period component. This identity is stored on both invoice-backed and charge-backed state. Duplicate identities, including a collision across the two backends, are an integrity error.

Backend ownership

Persisted artifact type determines which backend continues to own an existing item. New target items are routed according to the available charge stack, feature gates, settlement mode, and price type. This permits gradual movement from invoice-backed subscription items to charge-backed items without silently replacing already persisted ownership.

Charge-backed reconciliation is the replacement path for supported new artifacts. The invoice backend remains for compatibility while that transition is incomplete. Existing artifacts keep their persisted backend rather than being migrated implicitly.

Invoice-backed patches may create gathering lines or update mutable gathering and standard invoices. If subscription state disagrees with an immutable standard invoice, sync records validation issues; it does not rewrite the invoice.

Mutable legacy standard-line updates snapshot and rate through billing's legacy engine before submission. Immutable comparisons retain snapshot-only behavior; they do not change calculated invoice history. See Billing line ownership for the calculation and validation-issue contract.

Charge-backed patches operate on the charge intent. Charge-managed invoice lines are projections of charge state and are deliberately excluded from the subscription-sync invoice read model. A customer-facing charge override also does not erase the subscription-owned base intent: later subscription changes continue to reconcile that base without resurrecting the overridden effective charge. Credit-then-invoice usage reconciliation may preserve a missing feature or meter as a product-catalog issue on a newly created charge; invoice assignment keeps its gathering line pending until the dependency is repaired.

When a matched flat-fee or usage-based charge's physical phase or item reference changes, sync compares the subscription-owned base intent's billing terms. Currency identity, tax behavior and explicit tax-code ID, settlement mode, feature key, cost basis, price, discounts, proration, and usage unit conversion can distinguish a replacement charge. An omitted source tax-code ID accepts the charge's stored ID; flat-fee payment terms are compared after applying the catalog default. Presentation, annotations, discount lineage, expanded currency data, pinned feature ID, plan attribution, periods, and invoice scheduling do not distinguish billing terms. Historical plan and feature attribution remain on the existing charge. Charge ownership and root subscription identity are validated separately.

Equivalent terms permit repair of the physical reference, followed by normal period reconciliation. A changed service-period end can still shrink or extend the charge. An incompatible service-period start selects replacement because shrink and extend only move the end. When physical references already match, sync skips billing-term comparison and continues period reconciliation.

Before replacing a matched charge, sync loads all its realization runs, including older and deleted runs, and checks every referenced invoice. An immutable invoice blocks replacement, as does missing invoice history or a failed lookup. This check runs only for the matched-charge replacement decision. Cancellation deletes and ordinary period shrink or extend continue through their own paths.

This decision always follows system intent. Manual overrides remain associated with the retired or repaired charge and do not participate in comparison; users remain responsible for managing them. Charge deletion remains observable, while economic corrections for already realized credits or invoice history remain the responsibility of the downstream charge lifecycle rather than subscription sync.

Deletion, cancellation, and retries

Cancellation syncs through the subscription end so artifacts are shortened or removed according to the final desired periods. When a cancellation event's root UpdatedAt predates the current subscription, sync uses the current view and current cancellation end; a continued subscription needs no cancellation sync. Annotation writes can also advance the root timestamp, so age alone must not discard a still-valid cancellation. This timestamp is not a version for all child changes. A deleted subscription has no view and therefore produces an empty target, asking reconciliation to remove its remaining owned artifacts subject to immutable-invoice rules. Cleanup must use an ID-based entrypoint because the normal subscription view lookup excludes deleted records.

Sync state is written only after the plan has been applied. HasBillables prevents needless future scheduling; NextSyncAfter is the end of the generated horizon. Errors remain retryable through the event handler or periodic reconciler. Dry-run builds the same plan but only logs it and does not write artifacts or sync state.

Intentional limitations

  • invoice-backed subscription artifacts remain fiat-only; custom-currency items require the charges backend
  • charge intents preserve each subscription item's fiat or managed custom currency. For credit_then_invoice, subscription cost-basis mode maps to a dynamic charge cost basis or the subscription's pinned cost-basis resource; the charge lifecycle performs overage conversion into invoice currency
  • subscription-owned credit-purchase charges are unsupported
  • immutable invoice drift is reported, not automatically corrected
  • an asOf at the current instant is not a request to provision the entire future subscription; callers that need future artifacts must provide the corresponding horizon

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Adapter

type Adapter interface {
	SyncStateAdapter

	entutils.TxCreator
}

type EventHandler

type EventHandler interface {
	HandleCancelledEvent(ctx context.Context, event *subscription.CancelledEvent) error
	HandleDeletedEvent(ctx context.Context, event *subscription.DeletedEvent) error
	HandleSubscriptionSyncEvent(ctx context.Context, event *subscription.SubscriptionSyncEvent) error
	HandleInvoiceCreation(ctx context.Context, event *billing.StandardInvoiceCreatedEvent) error
}

type GetSyncStatesInput

type GetSyncStatesInput = []models.NamespacedID

type InvalidateSyncStateInput

type InvalidateSyncStateInput = models.NamespacedID

type Service

type Service interface {
	EventHandler
	SyncService
	SyncStateService
}

type SyncService

type SyncService interface {
	SyncByViewAndInvoiceCustomer(ctx context.Context, view subscription.SubscriptionView, asOf time.Time, opts ...SynchronizeSubscriptionOption) error
	SyncByIDAndInvoiceCustomer(ctx context.Context, subscriptionID models.NamespacedID, asOf time.Time, opts ...SynchronizeSubscriptionOption) error
	SyncByView(ctx context.Context, view subscription.SubscriptionView, asOf time.Time, opts ...SynchronizeSubscriptionOption) error
	SyncByID(ctx context.Context, subscriptionID models.NamespacedID, asOf time.Time, opts ...SynchronizeSubscriptionOption) error
}

type SyncState

type SyncState struct {
	SubscriptionID models.NamespacedID
	HasBillables   bool
	SyncedAt       time.Time
	NextSyncAfter  *time.Time
}

type SyncStateAdapter

type SyncStateAdapter interface {
	InvalidateSyncState(ctx context.Context, input InvalidateSyncStateInput) error
	GetSyncStates(ctx context.Context, input GetSyncStatesInput) ([]SyncState, error)
	UpsertSyncState(ctx context.Context, input UpsertSyncStateInput) error
}

type SyncStateService

type SyncStateService interface {
	GetSyncStates(ctx context.Context, input GetSyncStatesInput) ([]SyncState, error)
}

type SynchronizeSubscriptionOption

type SynchronizeSubscriptionOption func(*SynchronizeSubscriptionOptions)

func EnableDryRun

func EnableDryRun() SynchronizeSubscriptionOption

type SynchronizeSubscriptionOptions

type SynchronizeSubscriptionOptions struct {
	DryRun bool
}

type UpsertSyncStateInput

type UpsertSyncStateInput = SyncState

func (UpsertSyncStateInput) Validate

func (i UpsertSyncStateInput) Validate() error

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL