Skip to main content

Quote the plan. Wait for the webhook.

00:01:03:19

Polar has no quote API. immediateChargeAmount comes back as 0 even when Polar is about to invoice. If you apply the plan from that 200, you showed a price that isn't real, and you wrote entitlements before Polar did.

ts
const pendingPayment =
  ownedProvider.providerId === 'polar'
    ? !context.scheduleAtPeriodEnd
    : quote.immediateChargeAmount > 0;

Preview still matters. I mint a signed quote with the org, subscription, target plan version, Polar product id, discounts, and money fields. Five minutes, then it dies.

ts
const PLAN_CHANGE_QUOTE_TTL_MS = 5 * 60 * 1_000;

const quoteToken = mintPlanChangeQuote(
  {
    ...payload,
    issuedAt,
    expiresAt: issuedAt + PLAN_CHANGE_QUOTE_TTL_MS,
  },
  this.quoteSecret(),
);

On commit I verify the token, preview again, and reject if the money moved. Then I persist a pending intent so a fast plan_changed webhook can mark the row applied instead of racing the HTTP response.

ts
await this.planChangeRequests.upsertPending({
  organizationId: context.organizationId,
  provider: ownedProvider.providerId,
  providerSubscriptionId: context.providerSubscriptionId,
  targetPlanId: context.targetPlan.id,
  targetProviderProductId: targetProductId,
});

Mock billing is the only local apply

The mock adapter has no webhooks, so immediate changes write locally. Polar does not. Production stays pending until Polar writes.

I stopped flipping entitlements from the HTTP 200. Polar writes them.