Skip to content

Tiered SaaS

Basic and Pro belong to one subscription family. A plan change schedules terms; it does not grant access or create payment.

import {
Access,
Billing,
Limits,
Offer,
Purchase,
ScheduleSubscriptionPlanChangeCommand,
checkAccess,
createCheckout,
processPaymentNotification,
registerOffers,
scheduleSubscriptionPlanChange,
type SubscriptionPlanChangeQuote,
} from "@byfungsi/sapa";
const basicAccess =
yield *
Access.entitlements({
entitlements: ["workspace.use"],
duration: "purchased_period",
});
const basicLimit =
yield *
Limits.resource({
resource: "workspace.project",
quantity: 5,
entitlement: "workspace.use",
});
const basicPurchase =
yield *
Purchase.parse({
key: "workspace.plan",
revision: 1,
provides: [basicAccess, basicLimit],
});
const basicBilling =
yield *
Billing.subscription({
family: "workspace",
interval: "month",
price: { currency: "IDR", amount: 100_000n },
});
const basic =
yield *
Offer.parse({
key: "workspace.basic",
revision: 1,
purchase: basicPurchase,
billing: basicBilling,
});
const proAccess =
yield *
Access.entitlements({
entitlements: ["workspace.use", "analytics.advanced"],
duration: "purchased_period",
});
const proLimit =
yield *
Limits.resource({
resource: "workspace.project",
quantity: 100,
entitlement: "workspace.use",
});
const proPurchase =
yield *
Purchase.parse({
key: "workspace.plan",
revision: 2,
provides: [proAccess, proLimit],
});
const proBilling =
yield *
Billing.subscription({
family: "workspace",
interval: "month",
price: { currency: "IDR", amount: 300_000n },
});
const pro =
yield *
Offer.parse({
key: "workspace.pro",
revision: 1,
purchase: proPurchase,
billing: proBilling,
});
yield * registerOffers([basic, pro]);
yield *
createCheckout({
type: "offer",
offerKey: basic.key,
offerRevision: basic.revision,
quantity: null,
provider: "midtrans",
customer,
successUrl,
cancelUrl,
clientIp: null,
idempotencyKey: "workspace-basic-42",
});
// Build this quote server-side from the authenticated subscription head.
const scheduleUpgrade = (quote: SubscriptionPlanChangeQuote) =>
scheduleSubscriptionPlanChange(
ScheduleSubscriptionPlanChangeCommand.make({
quote,
command: {
idempotencyKey: "workspace-upgrade-42",
quoteId: quote.id,
expectedVersion: quote.expectedVersion,
},
}),
);
yield * scheduleUpgrade(acceptedProQuote);
yield *
createCheckout({
type: "subscription_renewal",
subscriptionId: acceptedProQuote.subscriptionId,
provider: "midtrans",
customer,
successUrl,
cancelUrl,
clientIp: null,
idempotencyKey: "workspace-renewal-42-2026-10",
});
yield *
processPaymentNotification({
provider: "midtrans",
payload: rawProviderPayload,
});
const hasPro =
yield *
checkAccess({
externalCustomerId: "customer-42",
entitlement: "analytics.advanced",
});

acceptedProQuote must retain current and target terms, the expected history version, next-period boundary, and expiry. Never accept a quote assembled by an untrusted browser. The upgrade becomes current only when the renewal settles.