dev - #2283
Conversation
feat(licenses): support license plan customization
Create customized license prices under each parent Stripe product and match back-synced subscription items against the effective license definition.
fix(licenses): scope custom stripe prices to parent products
|
Automatic Review Skipped Too many files for automatic review. If you would still like a review, you can trigger one manually by commenting: |
|
Capy auto-review is paused for this organization because the usage-cycle auto-review limit has been reached. Increase the limit or turn it off in billing settings to resume automatic reviews. |
|
The latest updates on your projects. Learn more about Vercel for GitHub. 2 Skipped Deployments
|
|
Deployment #708 deployment failed
Deployed on Manufact.com |
| void sendBillingUpdatedWebhook({ | ||
| ctx, | ||
| autumnBillingPlan, | ||
| originalFullCustomer: syncContext.fullCustomer, | ||
| tags, | ||
| }); | ||
|
|
||
| const customerProductUpdates = ( |
There was a problem hiding this comment.
Unconditional webhook now doubles events in
handleStripeSubscriptionUpdated
syncV2 previously guarded sendBillingUpdatedWebhook behind an opt-in webhook param. The old code comment said explicitly: "Webhook emission is opt-in via webhook so Stripe auto-sync callers (which already emit billing.updated from the originating action) don't double-send." That guard is now gone.
handleStripeSubscriptionUpdated calls autoSyncUpdatedSubscription (which runs syncV2 → emits webhook #1 with tags: ["sync:customer.subscription.updated"]), then calls emitBillingChangeWebhook unconditionally at line 94. Whenever expireRemovedCustomerProducts, syncCustomerProductStatus, handleStripeSubscriptionCanceled, or handleStripeSubscriptionRenewed track any change into the event context, the second emission fires and customers receive two billing.updated events for one Stripe event. The same issue affects autoSyncFromSubscription (subscription.created path) which now also fires a webhook where it previously fired none.
Prompt To Fix With AI
This is a comment left during a code review.
Path: server/src/internal/billing/v2/actions/sync/syncV2.ts
Line: 77-84
Comment:
**Unconditional webhook now doubles events in `handleStripeSubscriptionUpdated`**
`syncV2` previously guarded `sendBillingUpdatedWebhook` behind an opt-in `webhook` param. The old code comment said explicitly: _"Webhook emission is opt-in via `webhook` so Stripe auto-sync callers (which already emit billing.updated from the originating action) don't double-send."_ That guard is now gone.
`handleStripeSubscriptionUpdated` calls `autoSyncUpdatedSubscription` (which runs `syncV2` → emits webhook #1 with `tags: ["sync:customer.subscription.updated"]`), then calls `emitBillingChangeWebhook` unconditionally at line 94. Whenever `expireRemovedCustomerProducts`, `syncCustomerProductStatus`, `handleStripeSubscriptionCanceled`, or `handleStripeSubscriptionRenewed` track any change into the event context, the second emission fires and customers receive two `billing.updated` events for one Stripe event. The same issue affects `autoSyncFromSubscription` (subscription.created path) which now also fires a webhook where it previously fired none.
How can I resolve this? If you propose a fix, please make it concise.| }); | ||
| } | ||
|
|
||
| for (const update of autumnBillingPlan.customerLicenseUpdates ?? []) { | ||
| if (update.paidQuantity === undefined || !update.customerLicenseId) | ||
| continue; | ||
| const customerProduct = licenseParentById.get(update.customerLicenseId); | ||
| if (!customerProduct) continue; | ||
|
|
||
| entries.push({ | ||
| customerProduct, | ||
| change: { | ||
| action: "updated", | ||
| ...toCustomerPlanSnapshot({ cusProduct: customerProduct }), | ||
| previous_attributes: {}, |
There was a problem hiding this comment.
License seat update entries carry no change detail
The billing.updated plan-change entry for a license seat update always sets previous_attributes: {} and item_changes: []. Consumers can see that a customer product was "updated" but get no information about what changed (old/new paid seat counts). Consider populating previous_attributes with the previous paid_quantity from originalFullCustomer's customer license so the event is actionable.
Prompt To Fix With AI
This is a comment left during a code review.
Path: server/src/internal/billing/v2/utils/billingChangeResponse/buildPlanChanges.ts
Line: 307-321
Comment:
**License seat update entries carry no change detail**
The `billing.updated` plan-change entry for a license seat update always sets `previous_attributes: {}` and `item_changes: []`. Consumers can see that a customer product was `"updated"` but get no information about what changed (old/new paid seat counts). Consider populating `previous_attributes` with the previous `paid_quantity` from `originalFullCustomer`'s customer license so the event is actionable.
How can I resolve this? If you propose a fix, please make it concise.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| const customerLicenseUpdates: CustomerLicenseUpdate[] = []; | ||
|
|
||
| for (const productContext of immediatePhase.productContexts) { | ||
| const currentCustomerProduct = productContext.currentCustomerProduct; | ||
| if (currentCustomerProduct?.product_id === productContext.fullProduct.id) { | ||
| const licenseQuantityChanges = computeCustomerLicenseQuantityChanges({ | ||
| customerProduct: currentCustomerProduct, | ||
| customerLicenseQuantities: productContext.customerLicenseQuantities, | ||
| }); | ||
| if (licenseQuantityChanges.length > 0) { | ||
| customerLicenseUpdates.push( | ||
| ...licenseQuantityChanges.map(({ update }) => update), | ||
| ); |
There was a problem hiding this comment.
continue skips all product-level sync work when seat counts drift
When licenseQuantityChanges.length > 0 for a product context, the loop continues before reaching the initImmediateSyncCustomerProduct path. buildIncrementalSyncParams guarantees these paths are mutually exclusive for DIFFERENT plans (product-id changed → early-return before seat check). However, both a seat drift AND a full-product re-sync can be needed for the same Autumn plan id in the version-upgrade case, since the gate in buildIncrementalSyncParams compares product.id (public ID, stable across versions) not internal_id. If a new product version with changed items is also accompanied by a Stripe seat-quantity change, the version upgrade would be silently skipped here.
Prompt To Fix With AI
This is a comment left during a code review.
Path: server/src/internal/billing/v2/actions/sync/compute/computeSyncImmediatePhase.ts
Line: 83-95
Comment:
**`continue` skips all product-level sync work when seat counts drift**
When `licenseQuantityChanges.length > 0` for a product context, the loop `continue`s before reaching the `initImmediateSyncCustomerProduct` path. `buildIncrementalSyncParams` guarantees these paths are mutually exclusive for DIFFERENT plans (product-id changed → early-return before seat check). However, both a seat drift AND a full-product re-sync can be needed for the same Autumn plan id in the version-upgrade case, since the gate in `buildIncrementalSyncParams` compares `product.id` (public ID, stable across versions) not `internal_id`. If a new product version with changed items is also accompanied by a Stripe seat-quantity change, the version upgrade would be silently skipped here.
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
6 issues found and verified against the latest diff
Confidence score: 2/5
- In
server/src/internal/billing/v2/actions/sync/syncV2.tsandserver/src/internal/billing/v2/actions/sync/compute/computeSyncImmediatePhase.ts, license replacement/mixed sync paths can skip or misscustomerLicenseUpdates, which can leave stale assignments or drop custom license/item changes after sync — add reconciliation for replacement flows and remove thecontinuebypass so custom state is always initialized/collected before merging. shared/utils/fullSubjectUtils/fullSubjectToApiCustomerProducts.tsappears to remove a guard that filtered plan products, but entity-scoped rows may includecustomer_license_link_id; merging as-is risks misclassifying assigned license products as normal subscriptions/purchases in API output — restore the existing plan-product filter before spreading custom fields.vite/src/views/products/plan/components/SaveChangesBar.tsxno longer invalidateslicense_productson save, so UI selectors/tables can show stale license-linking state even when backend changes succeeded — keepinvalidateLicenseProducts()in the success invalidation list before merge.server/src/internal/products/repos/utils/composeFullProductQuery.tsandshared/api/products/crud/licenses/planLicenseParams.tsintroduce lower-severity API correctness issues (leaking internalentitlementRefs/priceRefsand missing preview diffs for customize-only edits), which can confuse clients and expose internal shape — strip those relation arrays during normalization and include customization change signals in preview responses.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="shared/api/products/crud/licenses/planLicenseParams.ts">
<violation number="1" location="shared/api/products/crud/licenses/planLicenseParams.ts:10">
P2: Plan previews report no change when only `customize` is edited, even though the update applies the customization. Including the effective customization (or a customization-change marker) in `previewPlanLicenseSync` comparison and output would keep previews aligned with the newly accepted input.</violation>
</file>
<file name="server/src/internal/products/repos/utils/composeFullProductQuery.ts">
<violation number="1" location="server/src/internal/products/repos/utils/composeFullProductQuery.ts:92">
P2: Normalized license responses now leak the internal `entitlementRefs` and `priceRefs` relation arrays because `normalizeLinkProduct` spreads the entire hydrated link. Destructuring these relations before normalization would keep the returned object aligned with `FullPlanLicense` and avoid exposing database junction rows.</violation>
</file>
<file name="shared/utils/fullSubjectUtils/fullSubjectToApiCustomerProducts.ts">
<violation number="1" location="shared/utils/fullSubjectUtils/fullSubjectToApiCustomerProducts.ts:11">
P1: The entity API now returns assigned license products as normal subscriptions or purchases. Entity-scoped rows can include `customer_license_link_id`, so retain the existing plan-product filter before spreading `customer_products` to keep assigned license products hidden from the API.</violation>
</file>
<file name="server/src/internal/billing/v2/actions/sync/syncV2.ts">
<violation number="1" location="server/src/internal/billing/v2/actions/sync/syncV2.ts:54">
P1: Licensed syncs that replace the current product do not reconcile license state: those plans have inserted/expired customer products but no `customerLicenseUpdates`, so this condition is false. The old license assignments can remain attached to the expired parent and produce stale seat access; the trigger should cover all sync mutations that add or expire license-bearing customer products, not only quantity updates.</violation>
</file>
<file name="server/src/internal/billing/v2/actions/sync/compute/computeSyncImmediatePhase.ts">
<violation number="1" location="server/src/internal/billing/v2/actions/sync/compute/computeSyncImmediatePhase.ts:96">
P1: A mixed sync can silently lose custom license/item changes when any existing license quantity also changes, because this `continue` bypasses product initialization and collection of `customPrices`, `customEntitlements`, and `insertPlanLicenses`. The optimization should only short-circuit a truly quantity-only context; otherwise preserve the normal initialization path while applying the in-place quantity updates.</violation>
</file>
<file name="vite/src/views/products/plan/components/SaveChangesBar.tsx">
<violation number="1" location="vite/src/views/products/plan/components/SaveChangesBar.tsx:149">
P2: License linking changes are not propagated to the `license_products` query after this save. Retaining `invalidateLicenseProducts()` in this success invalidation list would keep license selectors and license-product tables consistent with the saved `licenses` array.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| }): FullCusProduct[] => | ||
| fullSubject.subjectType === "entity" | ||
| ? [ | ||
| ...fullSubject.customer_products, |
There was a problem hiding this comment.
P1: The entity API now returns assigned license products as normal subscriptions or purchases. Entity-scoped rows can include customer_license_link_id, so retain the existing plan-product filter before spreading customer_products to keep assigned license products hidden from the API.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At shared/utils/fullSubjectUtils/fullSubjectToApiCustomerProducts.ts, line 11:
<comment>The entity API now returns assigned license products as normal subscriptions or purchases. Entity-scoped rows can include `customer_license_link_id`, so retain the existing plan-product filter before spreading `customer_products` to keep assigned license products hidden from the API.</comment>
<file context>
@@ -1,9 +1,18 @@
+}): FullCusProduct[] =>
+ fullSubject.subjectType === "entity"
+ ? [
+ ...fullSubject.customer_products,
+ // ...(fullSubject.aggregated_customer_products ?? []),
+ ]
</file context>
| ...fullSubject.customer_products, | |
| ...fullSubject.customer_products.filter( | |
| (customerProduct) => | |
| customerProduct.customer_license_link_id == null || | |
| customerProduct.internal_entity_id == null, | |
| ), |
|
|
||
| // 4. Execute | ||
| await executeAutumnBillingPlan({ ctx, autumnBillingPlan }); | ||
| if ((autumnBillingPlan.customerLicenseUpdates?.length ?? 0) > 0) { |
There was a problem hiding this comment.
P1: Licensed syncs that replace the current product do not reconcile license state: those plans have inserted/expired customer products but no customerLicenseUpdates, so this condition is false. The old license assignments can remain attached to the expired parent and produce stale seat access; the trigger should cover all sync mutations that add or expire license-bearing customer products, not only quantity updates.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/src/internal/billing/v2/actions/sync/syncV2.ts, line 54:
<comment>Licensed syncs that replace the current product do not reconcile license state: those plans have inserted/expired customer products but no `customerLicenseUpdates`, so this condition is false. The old license assignments can remain attached to the expired parent and produce stale seat access; the trigger should cover all sync mutations that add or expire license-bearing customer products, not only quantity updates.</comment>
<file context>
@@ -63,6 +51,13 @@ export const syncV2 = async ({
// 4. Execute
await executeAutumnBillingPlan({ ctx, autumnBillingPlan });
+ if ((autumnBillingPlan.customerLicenseUpdates?.length ?? 0) > 0) {
+ await reconcileLicenseStateForCustomer({
+ ctx,
</file context>
| customerLicenseUpdates.push( | ||
| ...licenseQuantityChanges.map(({ update }) => update), | ||
| ); | ||
| continue; |
There was a problem hiding this comment.
P1: A mixed sync can silently lose custom license/item changes when any existing license quantity also changes, because this continue bypasses product initialization and collection of customPrices, customEntitlements, and insertPlanLicenses. The optimization should only short-circuit a truly quantity-only context; otherwise preserve the normal initialization path while applying the in-place quantity updates.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/src/internal/billing/v2/actions/sync/compute/computeSyncImmediatePhase.ts, line 96:
<comment>A mixed sync can silently lose custom license/item changes when any existing license quantity also changes, because this `continue` bypasses product initialization and collection of `customPrices`, `customEntitlements`, and `insertPlanLicenses`. The optimization should only short-circuit a truly quantity-only context; otherwise preserve the normal initialization path while applying the in-place quantity updates.</comment>
<file context>
@@ -76,14 +80,29 @@ export const computeSyncImmediatePhase = ({
+ customerLicenseUpdates.push(
+ ...licenseQuantityChanges.map(({ update }) => update),
+ );
+ continue;
+ }
+ }
</file context>
| onSuccess: () => | ||
| Promise.all([ | ||
| invalidatePlanLicenses(), | ||
| invalidateProduct(), |
There was a problem hiding this comment.
P2: License linking changes are not propagated to the license_products query after this save. Retaining invalidateLicenseProducts() in this success invalidation list would keep license selectors and license-product tables consistent with the saved licenses array.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At vite/src/views/products/plan/components/SaveChangesBar.tsx, line 149:
<comment>License linking changes are not propagated to the `license_products` query after this save. Retaining `invalidateLicenseProducts()` in this success invalidation list would keep license selectors and license-product tables consistent with the saved `licenses` array.</comment>
<file context>
@@ -131,36 +125,32 @@ export const SaveChangesBar = ({
+ onSuccess: () =>
+ Promise.all([
+ invalidatePlanLicenses(),
+ invalidateProduct(),
+ invalidateProducts(),
+ ]),
</file context>
| license_plan_id: z.string(), | ||
| included: z.number().int().min(0).optional(), | ||
| prepaid_only: z.boolean().optional(), | ||
| customize: LicenseCustomizeSchema.nullish(), |
There was a problem hiding this comment.
P2: Plan previews report no change when only customize is edited, even though the update applies the customization. Including the effective customization (or a customization-change marker) in previewPlanLicenseSync comparison and output would keep previews aligned with the newly accepted input.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At shared/api/products/crud/licenses/planLicenseParams.ts, line 10:
<comment>Plan previews report no change when only `customize` is edited, even though the update applies the customization. Including the effective customization (or a customization-change marker) in `previewPlanLicenseSync` comparison and output would keep previews aligned with the newly accepted input.</comment>
<file context>
@@ -1,12 +1,13 @@
license_plan_id: z.string(),
included: z.number().int().min(0).optional(),
prepaid_only: z.boolean().optional(),
+ customize: LicenseCustomizeSchema.nullish(),
metadata: z.record(z.string(), z.unknown()).optional(),
version: z.number().int().min(1).optional().meta({
</file context>
| const normalizeLicenseProduct = ( | ||
| link: NonNullable<ProductWithLicenseRelations["licenses"]>[number], | ||
| ) => { | ||
| const baseLink = normalizeLinkProduct(link, link.product); | ||
| return { | ||
| ...normalizeLinkProduct( | ||
| link, | ||
| link.customized | ||
| ? { | ||
| ...link.product, | ||
| prices: link.priceRefs.map(({ price }) => price), | ||
| entitlements: link.entitlementRefs.map( | ||
| ({ entitlement }) => entitlement, | ||
| ), | ||
| } | ||
| : link.product, | ||
| ), | ||
| ...(link.customized ? { base_product: baseLink.product } : {}), |
There was a problem hiding this comment.
P2: Normalized license responses now leak the internal entitlementRefs and priceRefs relation arrays because normalizeLinkProduct spreads the entire hydrated link. Destructuring these relations before normalization would keep the returned object aligned with FullPlanLicense and avoid exposing database junction rows.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/src/internal/products/repos/utils/composeFullProductQuery.ts, line 92:
<comment>Normalized license responses now leak the internal `entitlementRefs` and `priceRefs` relation arrays because `normalizeLinkProduct` spreads the entire hydrated link. Destructuring these relations before normalization would keep the returned object aligned with `FullPlanLicense` and avoid exposing database junction rows.</comment>
<file context>
@@ -67,6 +89,27 @@ const normalizeLinkProduct = <T extends DbPlanLicense>(
},
});
+const normalizeLicenseProduct = (
+ link: NonNullable<ProductWithLicenseRelations["licenses"]>[number],
+) => {
</file context>
| const normalizeLicenseProduct = ( | |
| link: NonNullable<ProductWithLicenseRelations["licenses"]>[number], | |
| ) => { | |
| const baseLink = normalizeLinkProduct(link, link.product); | |
| return { | |
| ...normalizeLinkProduct( | |
| link, | |
| link.customized | |
| ? { | |
| ...link.product, | |
| prices: link.priceRefs.map(({ price }) => price), | |
| entitlements: link.entitlementRefs.map( | |
| ({ entitlement }) => entitlement, | |
| ), | |
| } | |
| : link.product, | |
| ), | |
| ...(link.customized ? { base_product: baseLink.product } : {}), | |
| const normalizeLicenseProduct = ( | |
| link: NonNullable<ProductWithLicenseRelations["licenses"]>[number], | |
| ) => { | |
| const { entitlementRefs, priceRefs, ...dbLink } = link; | |
| const baseLink = normalizeLinkProduct(dbLink, link.product); | |
| return { | |
| ...normalizeLinkProduct( | |
| dbLink, | |
| link.customized | |
| ? { | |
| ...link.product, | |
| prices: priceRefs.map(({ price }) => price), | |
| entitlements: entitlementRefs.map( | |
| ({ entitlement }) => entitlement, | |
| ), | |
| } | |
| : link.product, | |
| ), | |
| ...(link.customized ? { base_product: baseLink.product } : {}), | |
| }; | |
| }; |
|
Warning Review the following alerts detected in dependencies. According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.
|
…lock-minimal Fix concurrent migrations duplicating customer plans
Add license-aware plan update previews, preserve customer definitions across versioned and in-place edits, and surface the resulting catalog changes in the dashboard.
feat(licenses): support catalog license plan updates
Fix versioned license pricing in plan dashboard
fix(sync): preserve matched plan version
Summary by cubic
Adds parent-specific license customization and license-aware plan update previews that keep customer pools stable. Also scopes custom license Stripe prices to the parent product, unifies seat-quantity convergence in
syncV2, enrichesbilling.updated, serializes migrations per customer, and pins the matched plan version during sync.New Features
licenses[].customize; plan responses includecustomize.license_changeswith link-level diffs and effective plan changes.syncV2returnscustomerLicenseUpdates;billing.updatednow includes previous paid quantities for actionable seat-change events.Bug Fixes
syncV2preserves the matched plan version when exact Stripe Price IDs are used, preventing version drift.Written for commit 940bfad. Summary will update on new commits.
Greptile Summary
This PR ships two major features: (1) per-link license plan customization — parent plans can now carry their own item/price diff for each linked license, stored in
licenseEntitlements/licensePricesjunction tables and rebased automatically when the base license product changes; and (2) unification of license seat-quantity sync into the mainsyncV2action, removing the separatesyncLicenseQuantitiesaction.PlanLicenseParamsgains acustomizefield;syncPlanLicensesapplies preserve/clear/replace logic per link;rebaseCatalogPlanLicensespropagates base-product edits onto saved customizations; new Drizzle relations enable ORM loading of custom rows.syncLicenseQuantitiesabsorbed intosyncV2/computeSyncImmediatePhase: seat-count drifts detected inline ascustomerLicenseUpdates;reconcileLicenseStateForCustomerruns insidesyncV2when seat updates are present.ApiPlanLicenseV1andApiPlanItemV1gain internalentitlement_id/price_idfields;climbLicenseMatchnow returnskind: \"none\"for ambiguous multi-parent cases.Confidence Score: 3/5
The license-customization and seat-sync-unification paths look solid, but the always-on webhook in syncV2 may cause duplicate billing.updated events for customers on the subscription.updated path.
The license customization and seat-sync unification logic is well-tested and cleanly structured, but the removal of the webhook opt-in guard in syncV2 changes customer-observable behavior: handleStripeSubscriptionUpdated can now emit two separate billing.updated webhooks for a single Stripe event in any case where both a sync and tracked status/removal changes occur together.
server/src/internal/billing/v2/actions/sync/syncV2.ts and server/src/external/stripe/webhookHandlers/handleStripeSubscriptionUpdated/tasks/autoSyncUpdatedSubscription.ts warrant a second look to confirm the split-webhook behavior is intentional.
Important Files Changed
Sequence Diagram
%%{init: {'theme': 'neutral'}}%% sequenceDiagram participant Stripe participant SubUpdated as handleStripeSubscriptionUpdated participant AutoSync as autoSyncUpdatedSubscription participant SyncV2 as syncV2 participant EmitCtx as emitBillingChangeWebhook Stripe->>SubUpdated: subscription.updated SubUpdated->>SubUpdated: syncCustomerProductStatus (tracked to ctx) SubUpdated->>SubUpdated: handleStripeSubscriptionCanceled (tracked to ctx) SubUpdated->>AutoSync: autoSyncUpdatedSubscription AutoSync->>AutoSync: expireRemovedCustomerProducts (tracked to ctx) AutoSync->>SyncV2: syncV2(tags sync customer subscription updated) SyncV2->>SyncV2: executeAutumnBillingPlan SyncV2->>SyncV2: reconcileLicenseStateForCustomer SyncV2-->>Stripe: billing.updated 1 sync changes AutoSync-->>SubUpdated: return SubUpdated->>EmitCtx: emitBillingChangeWebhook EmitCtx-->>Stripe: billing.updated 2 ctx-tracked changes if any%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%% sequenceDiagram participant Stripe participant SubUpdated as handleStripeSubscriptionUpdated participant AutoSync as autoSyncUpdatedSubscription participant SyncV2 as syncV2 participant EmitCtx as emitBillingChangeWebhook Stripe->>SubUpdated: subscription.updated SubUpdated->>SubUpdated: syncCustomerProductStatus (tracked to ctx) SubUpdated->>SubUpdated: handleStripeSubscriptionCanceled (tracked to ctx) SubUpdated->>AutoSync: autoSyncUpdatedSubscription AutoSync->>AutoSync: expireRemovedCustomerProducts (tracked to ctx) AutoSync->>SyncV2: syncV2(tags sync customer subscription updated) SyncV2->>SyncV2: executeAutumnBillingPlan SyncV2->>SyncV2: reconcileLicenseStateForCustomer SyncV2-->>Stripe: billing.updated 1 sync changes AutoSync-->>SubUpdated: return SubUpdated->>EmitCtx: emitBillingChangeWebhook EmitCtx-->>Stripe: billing.updated 2 ctx-tracked changes if anyPrompt To Fix All With AI
Reviews (1): Last reviewed commit: "chore: 🤖 merge main into dev" | Re-trigger Greptile