nomkit 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/LICENSE.txt +21 -0
  2. package/dist/_virtual/_rolldown/runtime.js +27 -0
  3. package/dist/adapters/index.d.ts +15 -0
  4. package/dist/adapters/index.js +6 -0
  5. package/dist/cli/commands/push.d.ts +6 -0
  6. package/dist/cli/commands/push.js +143 -0
  7. package/dist/cli/index.d.ts +4 -0
  8. package/dist/cli/index.js +18 -0
  9. package/dist/cli/lib/collection_sync.d.ts +107 -0
  10. package/dist/cli/lib/collection_sync.js +158 -0
  11. package/dist/cli/lib/config_loader.d.ts +15 -0
  12. package/dist/cli/lib/config_loader.js +43 -0
  13. package/dist/cli/lib/hash.d.ts +22 -0
  14. package/dist/cli/lib/hash.js +63 -0
  15. package/dist/cli/lib/migrations.d.ts +6 -0
  16. package/dist/cli/lib/migrations.js +17 -0
  17. package/dist/client/index.d.ts +13 -0
  18. package/dist/client/index.js +34 -0
  19. package/dist/core/nomba_api/banks.d.ts +14 -0
  20. package/dist/core/nomba_api/banks.js +0 -0
  21. package/dist/core/nomba_api/charge-tokenized-card.d.ts +33 -0
  22. package/dist/core/nomba_api/charge-tokenized-card.js +0 -0
  23. package/dist/core/nomba_api/checkout.d.ts +44 -0
  24. package/dist/core/nomba_api/checkout.js +0 -0
  25. package/dist/core/nomba_api/get_checkout.d.ts +57 -0
  26. package/dist/core/nomba_api/get_checkout.js +0 -0
  27. package/dist/core/nomba_api/index.d.ts +313 -0
  28. package/dist/core/nomba_api/index.js +179 -0
  29. package/dist/core/nomba_api/lib/utils.d.ts +235 -0
  30. package/dist/core/nomba_api/lib/utils.js +313 -0
  31. package/dist/core/nomba_api/list-tokenized-cards.d.ts +24 -0
  32. package/dist/core/nomba_api/list-tokenized-cards.js +0 -0
  33. package/dist/core/nomba_api/token-manager/index.d.ts +51 -0
  34. package/dist/core/nomba_api/token-manager/index.js +109 -0
  35. package/dist/core/pg_db/index.d.ts +108 -0
  36. package/dist/core/pg_db/index.js +76 -0
  37. package/dist/core/pg_db/migrations/20260703085901_wealthy_blacklash/migration.sql +120 -0
  38. package/dist/core/pg_db/migrations/20260703085901_wealthy_blacklash/snapshot.json +1616 -0
  39. package/dist/core/pg_db/relations.d.ts +46 -0
  40. package/dist/core/pg_db/relations.js +83 -0
  41. package/dist/core/pg_db/schema.d.ts +1138 -0
  42. package/dist/core/pg_db/schema.js +124 -0
  43. package/dist/endpoints/customers/api.js +51 -0
  44. package/dist/endpoints/entitlements/api.js +42 -0
  45. package/dist/endpoints/routes.d.ts +15 -0
  46. package/dist/endpoints/routes.js +15 -0
  47. package/dist/endpoints/subscriptions/api.js +263 -0
  48. package/dist/endpoints/subscriptions/utils.js +105 -0
  49. package/dist/endpoints/webhooks/invoice/api.js +28 -0
  50. package/dist/endpoints/webhooks/nomba/api.js +76 -0
  51. package/dist/endpoints/webhooks/nomba/utils.js +36 -0
  52. package/dist/index.d.ts +204 -0
  53. package/dist/index.js +175 -0
  54. package/dist/lib/utils.d.ts +21 -0
  55. package/dist/lib/utils.js +41 -0
  56. package/dist/node_modules/.pnpm/@better-fetch_fetch@1.3.1/node_modules/@better-fetch/fetch/dist/index.js +475 -0
  57. package/dist/package.js +4 -0
  58. package/dist/queue/backends/pglite/backend.d.ts +43 -0
  59. package/dist/queue/backends/pglite/backend.js +33 -0
  60. package/dist/queue/backends/pglite/index.d.ts +4 -0
  61. package/dist/queue/backends/pglite/index.js +4 -0
  62. package/dist/queue/backends/pglite/migrations/schema.d.ts +4 -0
  63. package/dist/queue/backends/pglite/migrations/schema.js +37 -0
  64. package/dist/queue/backends/pglite/notification-channel.d.ts +17 -0
  65. package/dist/queue/backends/pglite/notification-channel.js +61 -0
  66. package/dist/queue/backends/pglite/repository.d.ts +38 -0
  67. package/dist/queue/backends/pglite/repository.js +299 -0
  68. package/dist/queue/backends/redis/index.d.ts +7 -0
  69. package/dist/queue/backends/redis/index.js +1 -0
  70. package/dist/queue/client/index.d.ts +12 -0
  71. package/dist/queue/client/index.js +31 -0
  72. package/dist/queue/endpoints/api.d.ts +53 -0
  73. package/dist/queue/endpoints/api.js +45 -0
  74. package/dist/queue/endpoints/routes.d.ts +32 -0
  75. package/dist/queue/endpoints/routes.js +5 -0
  76. package/dist/queue/init.d.ts +27 -0
  77. package/dist/queue/init.js +31 -0
  78. package/dist/queue/lib/billing.d.ts +25 -0
  79. package/dist/queue/lib/billing.js +87 -0
  80. package/dist/queue/lib/utils.d.ts +30 -0
  81. package/dist/queue/lib/utils.js +35 -0
  82. package/package.json +71 -0
@@ -0,0 +1,124 @@
1
+ import { __exportAll } from "../../_virtual/_rolldown/runtime.js";
2
+ import { bigint, index, integer, jsonb, pgEnum, pgTable, text, timestamp, uniqueIndex, uuid } from "drizzle-orm/pg-core";
3
+ //#region core/pg_db/schema.ts
4
+ var schema_exports = /* @__PURE__ */ __exportAll({
5
+ customers: () => customers,
6
+ entitlements: () => entitlements,
7
+ featureTypeEnum: () => featureTypeEnum,
8
+ features: () => features,
9
+ invoiceStatusEnum: () => invoiceStatusEnum,
10
+ invoices: () => invoices,
11
+ paymentMethods: () => paymentMethods,
12
+ productFeatures: () => productFeatures,
13
+ products: () => products,
14
+ subscriptionStatusEnum: () => subscriptionStatusEnum,
15
+ subscriptions: () => subscriptions
16
+ });
17
+ const createdAt = timestamp("created_at").notNull().$defaultFn(() => /* @__PURE__ */ new Date());
18
+ const updatedAt = timestamp("updated_at").notNull().$defaultFn(() => /* @__PURE__ */ new Date()).$onUpdateFn(() => /* @__PURE__ */ new Date());
19
+ const products = pgTable("nomkit_product", {
20
+ internalId: uuid("internal_id").defaultRandom().primaryKey(),
21
+ id: text("id").notNull(),
22
+ label: text("label").notNull(),
23
+ hash: text("product_hash").notNull(),
24
+ priceAmount: integer("price_amount"),
25
+ priceInterval: text("product_interval"),
26
+ version: integer("version").notNull().default(1),
27
+ createdAt,
28
+ updatedAt
29
+ }, (table) => [index("product_idx").on(table.id), uniqueIndex("product_hash_version_unique").on(table.id, table.hash, table.version)]);
30
+ const features = pgTable("nomkit_feature", {
31
+ internalId: uuid("internal_id").defaultRandom().primaryKey(),
32
+ id: text("id").notNull(),
33
+ label: text("label").notNull(),
34
+ createdAt,
35
+ updatedAt
36
+ }, (table) => [index("feature_idx").on(table.internalId), uniqueIndex("feature_unique").on(table.id)]);
37
+ const featureTypeEnum = pgEnum("feature_type", ["boolean", "metered"]);
38
+ const productFeatures = pgTable("nomkit_product_features", {
39
+ internalId: uuid("id").defaultRandom().primaryKey(),
40
+ featureInternalId: uuid("feature_internal_id").notNull().references(() => features.internalId),
41
+ type: featureTypeEnum("type").notNull(),
42
+ limit: bigint("limit", { mode: "bigint" }),
43
+ resetInterval: text("reset_interval"),
44
+ hash: text("feature_hash").notNull(),
45
+ productId: text("product_id").notNull(),
46
+ productVersion: integer("product_version").notNull(),
47
+ createdAt,
48
+ updatedAt
49
+ }, (table) => [index("project_feature_idx").on(table.internalId), uniqueIndex("product_feature_unique_version").on(table.internalId, table.productId, table.featureInternalId, table.productVersion)]);
50
+ const customers = pgTable("nomkit_customer", {
51
+ id: uuid("id").defaultRandom().primaryKey(),
52
+ name: text("name"),
53
+ email: text("email").unique().notNull(),
54
+ metadata: jsonb("metadata").$type(),
55
+ externalId: text("external_id").unique(),
56
+ createdAt,
57
+ updatedAt
58
+ }, (table) => [index("customer_created_at_id_idx").on(table.createdAt, table.id)]);
59
+ const paymentMethods = pgTable("nomkit_payment_method", {
60
+ id: uuid("id").defaultRandom().primaryKey(),
61
+ customerId: uuid("customer_id").notNull().references(() => customers.id, { onDelete: "cascade" }),
62
+ tokenKey: text("token_key").notNull().unique(),
63
+ email: text("email").notNull(),
64
+ cardType: text("card_type").notNull(),
65
+ cardPan: text("card_pan").notNull(),
66
+ tokenExpirationDate: text("token_expiration_date").notNull(),
67
+ createdAt,
68
+ updatedAt
69
+ });
70
+ const subscriptionStatusEnum = pgEnum("subscription_status", [
71
+ "active",
72
+ "cancelled",
73
+ "past_due",
74
+ "pending",
75
+ "scheduled"
76
+ ]);
77
+ const subscriptions = pgTable("nomkit_subscription", {
78
+ id: uuid("id").defaultRandom().primaryKey(),
79
+ customerId: uuid("customer_id").notNull().references(() => customers.id, { onDelete: "cascade" }),
80
+ productInternalId: uuid("product_internal_id").notNull().references(() => products.internalId, { onDelete: "cascade" }),
81
+ paymentMethodId: uuid("payment_method_id").references(() => paymentMethods.id),
82
+ nextSubscriptionId: uuid("next_subscription_id").references(() => subscriptions.id, { onDelete: "set null" }),
83
+ status: subscriptionStatusEnum("status").notNull().default("active"),
84
+ currentPeriodStart: timestamp("current_period_start"),
85
+ currentPeriodEnd: timestamp("current_period_end"),
86
+ createdAt,
87
+ updatedAt,
88
+ endedAt: timestamp("ended_at")
89
+ }, (table) => [index("subscription_customer_idx").on(table.customerId)]);
90
+ const entitlements = pgTable("nomkit_entitlement", {
91
+ id: uuid("id").defaultRandom().primaryKey(),
92
+ subscriptionId: uuid("subscription_id").notNull().references(() => subscriptions.id, { onDelete: "cascade" }),
93
+ customerId: uuid("customer_id").notNull().references(() => customers.id, { onDelete: "cascade" }),
94
+ featureInternalId: uuid("feature_internal_id").notNull().references(() => features.internalId, { onDelete: "cascade" }),
95
+ limit: bigint("limit", { mode: "bigint" }),
96
+ remaining: bigint("remaining", { mode: "bigint" }),
97
+ resetAt: timestamp("reset_at"),
98
+ createdAt,
99
+ updatedAt
100
+ }, (table) => [
101
+ index("entitlement_customer_idx").on(table.customerId),
102
+ index("entitlement_subscription_idx").on(table.subscriptionId),
103
+ uniqueIndex("subscription_feature_unique").on(table.subscriptionId, table.featureInternalId)
104
+ ]);
105
+ const invoiceStatusEnum = pgEnum("invoice_status", [
106
+ "paid",
107
+ "failed",
108
+ "void",
109
+ "pending"
110
+ ]);
111
+ const invoices = pgTable("nomkit_invoice", {
112
+ id: uuid("id").defaultRandom().primaryKey(),
113
+ customerId: uuid("customer_id").notNull().references(() => customers.id),
114
+ subscriptionId: uuid("subscription_id").notNull().references(() => subscriptions.id),
115
+ status: invoiceStatusEnum("status").notNull().default("pending"),
116
+ amount: integer("amount").notNull(),
117
+ currency: text("currency").notNull(),
118
+ periodStart: timestamp("period_start"),
119
+ periodEnd: timestamp("period_end"),
120
+ createdAt,
121
+ updatedAt
122
+ }, (table) => [uniqueIndex("invoice_period_unique").on(table.subscriptionId, table.periodStart, table.periodEnd)]);
123
+ //#endregion
124
+ export { customers, entitlements, featureTypeEnum, features, invoiceStatusEnum, invoices, paymentMethods, productFeatures, products, schema_exports, subscriptionStatusEnum, subscriptions };
@@ -0,0 +1,51 @@
1
+ import { __exportAll } from "../../_virtual/_rolldown/runtime.js";
2
+ import { defineNomkitMethod } from "../../lib/utils.js";
3
+ import { z as z$1 } from "zod";
4
+ //#region endpoints/customers/api.ts
5
+ var api_exports = /* @__PURE__ */ __exportAll({
6
+ deleteCustomer: () => deleteCustomer,
7
+ getCustomer: () => getCustomer,
8
+ listCustomers: () => listCustomers,
9
+ upsertCustomer: () => upsertCustomer
10
+ });
11
+ const getCustomer = defineNomkitMethod({
12
+ input: z$1.object({
13
+ customerId: z$1.string().optional(),
14
+ externalId: z$1.string().optional()
15
+ }),
16
+ route: { path: "/customers/get" }
17
+ }, async (ctx) => {
18
+ ctx.nomkit.database;
19
+ return {};
20
+ });
21
+ const upsertCustomer = defineNomkitMethod({
22
+ input: z$1.object({
23
+ name: z$1.string().optional(),
24
+ externalId: z$1.string().min(1).max(60).optional(),
25
+ email: z$1.email().min(1).max(60),
26
+ metadata: z$1.union([z$1.record(z$1.string(), z$1.string()), z$1.null()]).default(null)
27
+ }),
28
+ route: { path: "/customers/upsert" }
29
+ }, async (ctx) => {
30
+ ctx.nomkit.database;
31
+ return true;
32
+ });
33
+ const listCustomers = defineNomkitMethod({
34
+ input: z$1.object({
35
+ cursor: z$1.string().optional(),
36
+ limit: z$1.number().default(20)
37
+ }),
38
+ route: { path: "/customers/list" }
39
+ }, async (ctx) => {
40
+ ctx.nomkit.database;
41
+ return [];
42
+ });
43
+ const deleteCustomer = defineNomkitMethod({
44
+ input: z$1.object({ customerId: z$1.string().optional() }),
45
+ route: { path: "/customers/delete" }
46
+ }, async (ctx) => {
47
+ ctx.nomkit.database;
48
+ return true;
49
+ });
50
+ //#endregion
51
+ export { api_exports, deleteCustomer, getCustomer, listCustomers, upsertCustomer };
@@ -0,0 +1,42 @@
1
+ import { __exportAll } from "../../_virtual/_rolldown/runtime.js";
2
+ import { defineNomkitMethod } from "../../lib/utils.js";
3
+ import { z as z$1 } from "zod";
4
+ //#region endpoints/entitlements/api.ts
5
+ var api_exports = /* @__PURE__ */ __exportAll({
6
+ checkEntitlements: () => checkEntitlements,
7
+ consumeEntitlements: () => consumeEntitlements
8
+ });
9
+ const checkEntitlements = defineNomkitMethod({
10
+ input: z$1.object({
11
+ customerId: z$1.string(),
12
+ featureId: z$1.string()
13
+ }),
14
+ route: { path: "/entitlements/check" }
15
+ }, async (ctx) => {
16
+ return ctx.input.featureId === "bluetick" ? { allowed: true } : {
17
+ allowed: true,
18
+ balance: {
19
+ limit: 100,
20
+ remaining: 99,
21
+ resetAt: (/* @__PURE__ */ new Date()).toUTCString(),
22
+ unlimited: false
23
+ }
24
+ };
25
+ });
26
+ const consumeEntitlements = defineNomkitMethod({
27
+ input: z$1.object({
28
+ customerId: z$1.string(),
29
+ featureId: z$1.string(),
30
+ amount: z$1.number()
31
+ }),
32
+ route: { path: "/entitlements/consume" }
33
+ }, async (ctx) => {
34
+ return {
35
+ featureId: ctx.input.featureId,
36
+ balance: 0,
37
+ limit: 100,
38
+ unlimited: false
39
+ };
40
+ });
41
+ //#endregion
42
+ export { api_exports, checkEntitlements, consumeEntitlements };
@@ -0,0 +1,15 @@
1
+ //#region endpoints/routes.d.ts
2
+ declare const nomkitWebhooks: {
3
+ readonly invoiceWebhook: import("better-call").Endpoint<"/invoice-webhook", "POST", {
4
+ billing: Record<string, never>;
5
+ signature: string;
6
+ status: "success" | "cancelled" | "retrying";
7
+ policy: string;
8
+ }, Record<string, any> | undefined, [], {
9
+ status: string;
10
+ }, {
11
+ scope: "http";
12
+ }, undefined>;
13
+ };
14
+ //#endregion
15
+ export { nomkitWebhooks };
@@ -0,0 +1,15 @@
1
+ import { api_exports } from "./customers/api.js";
2
+ import { api_exports as api_exports$1 } from "./entitlements/api.js";
3
+ import { api_exports as api_exports$2 } from "./subscriptions/api.js";
4
+ import { api_exports as api_exports$3 } from "./webhooks/invoice/api.js";
5
+ import { api_exports as api_exports$4 } from "./webhooks/nomba/api.js";
6
+ ({ ...api_exports$3 });
7
+ const routes = {
8
+ ...api_exports$2,
9
+ ...api_exports$1,
10
+ ...api_exports,
11
+ ...api_exports$4,
12
+ ...api_exports$3
13
+ };
14
+ //#endregion
15
+ export { routes };
@@ -0,0 +1,263 @@
1
+ import { __exportAll } from "../../_virtual/_rolldown/runtime.js";
2
+ import { subscriptions } from "../../core/pg_db/schema.js";
3
+ import { defineNomkitMethod, getOrderReference } from "../../lib/utils.js";
4
+ import { buildBaseCheckoutMetadata, classifyChange, computeUpgradeProration, getCurrentSubscription, getScheduledPeriodBounds, isFreePlan, persistProrationOnCustomer } from "./utils.js";
5
+ import { z as z$1 } from "zod";
6
+ import { and, eq } from "drizzle-orm";
7
+ //#region endpoints/subscriptions/api.ts
8
+ var api_exports = /* @__PURE__ */ __exportAll({
9
+ mockSubscribe: () => mockSubscribe,
10
+ subscribe: () => subscribe
11
+ });
12
+ const SubscribeSchema = z$1.object({
13
+ customerId: z$1.string().min(1).max(60),
14
+ planID: z$1.string().min(1).max(60),
15
+ successURL: z$1.url().min(1).max(60),
16
+ cancelURL: z$1.string().optional()
17
+ });
18
+ const subscribe = defineNomkitMethod({
19
+ input: SubscribeSchema,
20
+ route: { path: "/subscribe" }
21
+ }, async (ctx) => {
22
+ const db = ctx.nomkit.database;
23
+ const input = ctx.input;
24
+ const nombaAPI = ctx.nomkit.nomba;
25
+ const customer = await db.query.customers.findFirst({ where: { id: input.customerId } });
26
+ if (!customer) {
27
+ console.log("customer not found");
28
+ return null;
29
+ }
30
+ const plan = await db.query.products.findFirst({
31
+ where: { id: input.planID },
32
+ orderBy: (products, { desc }) => [desc(products.version)]
33
+ });
34
+ if (!plan) {
35
+ console.log("Plan not found");
36
+ return null;
37
+ }
38
+ const currentSubscription = await getCurrentSubscription(db, customer.id);
39
+ if (!currentSubscription) return startNewSubscription(db, customer, plan, nombaAPI);
40
+ if (currentSubscription.productInternalId === plan.internalId) {
41
+ console.log("Already on this plan");
42
+ return { checkoutURL: null };
43
+ }
44
+ const previousPlan = await db.query.products.findFirst({ where: { internalId: currentSubscription.productInternalId } });
45
+ if (!previousPlan) {
46
+ console.log("Failed to query previous plan");
47
+ return null;
48
+ }
49
+ return switchSubscriptionPlan(db, {
50
+ customer,
51
+ plan,
52
+ previousPlan,
53
+ currentSubscription,
54
+ nomba: nombaAPI
55
+ });
56
+ });
57
+ const mockSubscribe = defineNomkitMethod({
58
+ input: SubscribeSchema,
59
+ route: { path: "/mmock-subscribe" }
60
+ }, async (ctx) => {
61
+ const db = ctx.nomkit.database;
62
+ const input = ctx.input;
63
+ const nombaAPI = ctx.nomkit.nomba;
64
+ const customer = await db.query.customers.findFirst({ where: { id: input.customerId } });
65
+ if (!customer) {
66
+ console.log("customer not found");
67
+ return null;
68
+ }
69
+ const plan = await db.query.products.findFirst({
70
+ where: { id: input.planID },
71
+ orderBy: (products, { desc }) => [desc(products.version)]
72
+ });
73
+ if (!plan) {
74
+ console.log("Plan not found");
75
+ return null;
76
+ }
77
+ const currentSubscription = await getCurrentSubscription(db, customer.id);
78
+ if (!currentSubscription) return startNewSubscription(db, customer, plan, nombaAPI);
79
+ if (currentSubscription.productInternalId === plan.internalId) {
80
+ console.log("Already on this plan");
81
+ return { checkoutURL: null };
82
+ }
83
+ const previousPlan = await db.query.products.findFirst({ where: { internalId: currentSubscription.productInternalId } });
84
+ if (!previousPlan) {
85
+ console.log("Failed to query previous plan");
86
+ return null;
87
+ }
88
+ return switchSubscriptionPlan(db, {
89
+ customer,
90
+ plan,
91
+ previousPlan,
92
+ currentSubscription,
93
+ nomba: nombaAPI
94
+ });
95
+ });
96
+ async function startNewSubscription(db, customer, plan, nomba) {
97
+ const newSubscription = {
98
+ customerId: customer.id,
99
+ productInternalId: plan.internalId,
100
+ status: "pending"
101
+ };
102
+ const [subscription] = await db.insert(subscriptions).values(newSubscription).returning();
103
+ const amount = plan.priceAmount;
104
+ if (!amount) {
105
+ await db.update(subscriptions).set({
106
+ status: "active",
107
+ currentPeriodStart: /* @__PURE__ */ new Date()
108
+ }).where(eq(subscriptions.id, subscription.id));
109
+ return { checkoutURL: null };
110
+ }
111
+ const reference = getOrderReference();
112
+ const checkout = await nomba.checkOut({
113
+ amount: amount.toString(),
114
+ customerEmail: customer.email,
115
+ reference,
116
+ successURL: "input.successURL",
117
+ metadata: buildBaseCheckoutMetadata({
118
+ reference,
119
+ subscriptionId: subscription.id,
120
+ customer,
121
+ plan,
122
+ kind: "new"
123
+ })
124
+ });
125
+ console.log("Checkout for brand new subscription");
126
+ return { checkoutURL: checkout.checkoutLink };
127
+ }
128
+ async function activateSwitchedSubscriptionWithCredit(db, params) {
129
+ const { subscriptionId, previousSubscriptionId, now, periodEnd } = params;
130
+ await db.update(subscriptions).set({
131
+ status: "active",
132
+ currentPeriodStart: new Date(now.epochMilliseconds),
133
+ currentPeriodEnd: new Date(periodEnd.epochMilliseconds)
134
+ }).where(eq(subscriptions.id, subscriptionId));
135
+ await db.update(subscriptions).set({
136
+ status: "cancelled",
137
+ endedAt: new Date(now.epochMilliseconds)
138
+ }).where(eq(subscriptions.id, previousSubscriptionId));
139
+ }
140
+ async function switchSubscriptionPlan(db, params) {
141
+ const { customer, plan, previousPlan, currentSubscription } = params;
142
+ if (isFreePlan(previousPlan) || !currentSubscription.currentPeriodEnd) return switchSubscriptionImmediately(db, {
143
+ customer,
144
+ plan,
145
+ previousPlan,
146
+ currentSubscription,
147
+ prorate: false,
148
+ nomba: params.nomba
149
+ });
150
+ if (classifyChange(previousPlan, plan) === "upgrade") return switchSubscriptionImmediately(db, {
151
+ customer,
152
+ plan,
153
+ previousPlan,
154
+ currentSubscription,
155
+ prorate: true,
156
+ nomba: params.nomba
157
+ });
158
+ return scheduleSubscriptionSwitch(db, {
159
+ customer,
160
+ plan,
161
+ previousPlan,
162
+ currentSubscription
163
+ });
164
+ }
165
+ async function switchSubscriptionImmediately(db, params) {
166
+ const { customer, plan, previousPlan, currentSubscription, prorate } = params;
167
+ if (params.previousPlan) await db.update(subscriptions).set({ status: "cancelled" }).where(and(eq(subscriptions.productInternalId, params.previousPlan.internalId), eq(subscriptions.status, "active")));
168
+ if (currentSubscription.nextSubscriptionId) {
169
+ await db.delete(subscriptions).where(eq(subscriptions.id, currentSubscription.nextSubscriptionId));
170
+ await db.update(subscriptions).set({ nextSubscriptionId: null }).where(eq(subscriptions.id, currentSubscription.id));
171
+ }
172
+ const newSubscription = {
173
+ customerId: customer.id,
174
+ productInternalId: plan.internalId,
175
+ status: isFreePlan(plan) ? "active" : "pending"
176
+ };
177
+ const [subscription] = await db.insert(subscriptions).values(newSubscription).returning();
178
+ if (isFreePlan(plan)) {
179
+ await db.update(subscriptions).set({ currentPeriodStart: /* @__PURE__ */ new Date() }).where(eq(subscriptions.id, subscription.id));
180
+ await db.update(subscriptions).set({
181
+ status: "cancelled",
182
+ endedAt: /* @__PURE__ */ new Date()
183
+ }).where(eq(subscriptions.id, currentSubscription.id));
184
+ return { checkoutURL: null };
185
+ }
186
+ if (!prorate) {
187
+ const reference = getOrderReference();
188
+ return { checkoutURL: (await params.nomba.checkOut({
189
+ amount: (plan.priceAmount ?? 0).toString(),
190
+ customerEmail: customer.email,
191
+ reference,
192
+ successURL: "input.successURL",
193
+ metadata: {
194
+ ...buildBaseCheckoutMetadata({
195
+ reference,
196
+ subscriptionId: subscription.id,
197
+ customer,
198
+ plan,
199
+ kind: "new"
200
+ }),
201
+ "nomkit.subscription.previous_subscription_id": currentSubscription.id
202
+ }
203
+ })).checkoutLink };
204
+ }
205
+ const existingMetadata = customer.metadata ?? {};
206
+ const proration = computeUpgradeProration({
207
+ currentSubscription,
208
+ previousPlan,
209
+ plan,
210
+ existingBalance: Number(existingMetadata?.creditBalance ?? "0") || 0
211
+ });
212
+ await persistProrationOnCustomer(db, customer, previousPlan, plan, proration);
213
+ if (proration.chargeAmount <= 0) {
214
+ await activateSwitchedSubscriptionWithCredit(db, {
215
+ subscriptionId: subscription.id,
216
+ previousSubscriptionId: currentSubscription.id,
217
+ plan,
218
+ now: proration.now,
219
+ periodEnd: proration.periodEnd
220
+ });
221
+ console.log("Upgrade applied using existing credit balance");
222
+ return { checkoutURL: null };
223
+ }
224
+ const reference = getOrderReference();
225
+ const checkoutMetadata = {
226
+ ...buildBaseCheckoutMetadata({
227
+ reference,
228
+ subscriptionId: subscription.id,
229
+ customer,
230
+ plan,
231
+ kind: "upgrade"
232
+ }),
233
+ "nomkit.subscription.previous_subscription_id": currentSubscription.id,
234
+ "nomkit.subscription.previous_internalPlanId": previousPlan.internalId,
235
+ "nomkit.subscription.prorated_credit_applied": proration.totalCredit.toString()
236
+ };
237
+ return { checkoutURL: (await params.nomba.checkOut({
238
+ amount: proration.chargeAmount.toString(),
239
+ customerEmail: customer.email,
240
+ reference,
241
+ successURL: "input.successURL",
242
+ metadata: checkoutMetadata
243
+ })).checkoutLink };
244
+ }
245
+ async function scheduleSubscriptionSwitch(db, params) {
246
+ const { customer, plan, previousPlan, currentSubscription } = params;
247
+ const periodEnd = currentSubscription.currentPeriodEnd;
248
+ if (currentSubscription.nextSubscriptionId) await db.delete(subscriptions).where(eq(subscriptions.id, currentSubscription.nextSubscriptionId));
249
+ const { start, end } = getScheduledPeriodBounds(periodEnd, plan);
250
+ const newSubscription = {
251
+ customerId: customer.id,
252
+ productInternalId: plan.internalId,
253
+ status: "scheduled",
254
+ currentPeriodStart: start,
255
+ currentPeriodEnd: end
256
+ };
257
+ const [scheduled] = await db.insert(subscriptions).values(newSubscription).returning();
258
+ await db.update(subscriptions).set({ nextSubscriptionId: scheduled.id }).where(eq(subscriptions.id, currentSubscription.id));
259
+ console.log(`Scheduled switch from plan ${previousPlan.id} to ${plan.id}, effective ${periodEnd.toISOString()}`);
260
+ return { checkoutURL: null };
261
+ }
262
+ //#endregion
263
+ export { api_exports, mockSubscribe, subscribe };
@@ -0,0 +1,105 @@
1
+ import { customers, subscriptions } from "../../core/pg_db/schema.js";
2
+ import ms from "ms";
3
+ import { Temporal } from "temporal-polyfill";
4
+ import { and, desc, eq, inArray } from "drizzle-orm";
5
+ //#region endpoints/subscriptions/utils.ts
6
+ function getPeriodEnd(args) {
7
+ const periodStart = Temporal.Instant.from(args.now);
8
+ const milliseconds = ms(args.interval);
9
+ if (typeof milliseconds !== "number" || Number.isNaN(milliseconds)) throw new Error("You have an invalid interval");
10
+ if (milliseconds < 0) throw new Error("timestamp cannot include negative intervals");
11
+ return periodStart.add(Temporal.Duration.from({ milliseconds }));
12
+ }
13
+ function getScheduledPeriodBounds(periodEnd, plan) {
14
+ if (isFreePlan(plan)) return {
15
+ start: periodEnd,
16
+ end: null
17
+ };
18
+ const scheduledEnd = getPeriodEnd({
19
+ now: Temporal.Instant.fromEpochMilliseconds(periodEnd.getTime()),
20
+ interval: plan.priceInterval
21
+ });
22
+ return {
23
+ start: periodEnd,
24
+ end: new Date(scheduledEnd.epochMilliseconds)
25
+ };
26
+ }
27
+ function isFreePlan(plan) {
28
+ return plan.priceAmount === null && plan.priceInterval === null;
29
+ }
30
+ function classifyChange(previousPlan, plan) {
31
+ const oldAmount = previousPlan.priceAmount ?? 0;
32
+ const newAmount = plan.priceAmount ?? 0;
33
+ if (newAmount > oldAmount) return "upgrade";
34
+ if (newAmount < oldAmount) return "downgrade";
35
+ return "lateral";
36
+ }
37
+ async function getCurrentSubscription(db, customerId) {
38
+ const [currentSubscription] = await db.select().from(subscriptions).where(and(eq(subscriptions.customerId, customerId), inArray(subscriptions.status, [
39
+ "active",
40
+ "pending",
41
+ "scheduled"
42
+ ]))).orderBy(desc(subscriptions.createdAt)).limit(1);
43
+ return currentSubscription ?? null;
44
+ }
45
+ function buildBaseCheckoutMetadata(params) {
46
+ const { reference, subscriptionId, customer, plan, kind } = params;
47
+ return {
48
+ "nomkit.order_reference": reference,
49
+ "nomkit.subscription.id": subscriptionId,
50
+ "nomkit.subscription.customer_id": customer.id,
51
+ "nomkit.subscription.customer_email": customer.email,
52
+ "nomkit.subscription.internalPlanId": plan.internalId,
53
+ "nomkit.subscription.kind": kind
54
+ };
55
+ }
56
+ function resolveCurrentPeriod(currentSubscription, plan, now) {
57
+ const periodStart = currentSubscription.currentPeriodStart ? Temporal.Instant.fromEpochMilliseconds(currentSubscription.currentPeriodStart.getTime()) : now;
58
+ let periodEnd;
59
+ if (currentSubscription.currentPeriodEnd) periodEnd = Temporal.Instant.fromEpochMilliseconds(currentSubscription.currentPeriodEnd.getTime());
60
+ else periodEnd = getPeriodEnd({
61
+ now,
62
+ interval: plan.priceInterval
63
+ });
64
+ return {
65
+ periodStart,
66
+ periodEnd
67
+ };
68
+ }
69
+ function computeUpgradeProration(params) {
70
+ const { currentSubscription, previousPlan, plan, existingBalance } = params;
71
+ const oldAmount = previousPlan.priceAmount ?? 0;
72
+ const newAmount = plan.priceAmount ?? 0;
73
+ const now = Temporal.Now.instant();
74
+ const { periodStart, periodEnd } = resolveCurrentPeriod(currentSubscription, plan, now);
75
+ const totalPeriodMs = Math.max(0, periodStart.until(periodEnd).total("milliseconds"));
76
+ const remainingMs = Math.max(0, Math.min(totalPeriodMs, now.until(periodEnd).total("milliseconds")));
77
+ const remainingRatio = totalPeriodMs > 0 ? remainingMs / totalPeriodMs : 0;
78
+ const unusedAmount = Math.round(oldAmount * remainingRatio);
79
+ const proratedNewAmount = Math.round(newAmount * remainingRatio);
80
+ const totalCredit = existingBalance + unusedAmount;
81
+ const amountDue = proratedNewAmount - totalCredit;
82
+ return {
83
+ now,
84
+ periodEnd,
85
+ unusedAmount,
86
+ proratedNewAmount,
87
+ totalCredit,
88
+ chargeAmount: amountDue > 0 ? amountDue : 0,
89
+ newBalance: amountDue < 0 ? Math.abs(amountDue) : 0
90
+ };
91
+ }
92
+ async function persistProrationOnCustomer(db, customer, previousPlan, plan, proration) {
93
+ const updatedMetadata = {
94
+ ...customer.metadata ?? {},
95
+ creditBalance: proration.newBalance.toString(),
96
+ lastProrationAt: proration.now.toString(),
97
+ lastProrationFromPlan: previousPlan.id,
98
+ lastProrationToPlan: plan.id,
99
+ lastProrationUnusedAmount: proration.unusedAmount.toString(),
100
+ lastProrationChargeAmount: proration.chargeAmount.toString()
101
+ };
102
+ await db.update(customers).set({ metadata: updatedMetadata }).where(eq(customers.id, customer.id));
103
+ }
104
+ //#endregion
105
+ export { buildBaseCheckoutMetadata, classifyChange, computeUpgradeProration, getCurrentSubscription, getPeriodEnd, getScheduledPeriodBounds, isFreePlan, persistProrationOnCustomer, resolveCurrentPeriod };
@@ -0,0 +1,28 @@
1
+ import { __exportAll } from "../../../_virtual/_rolldown/runtime.js";
2
+ import { defineNomkitMethod } from "../../../lib/utils.js";
3
+ import { z as z$1 } from "zod";
4
+ //#region endpoints/webhooks/invoice/api.ts
5
+ var api_exports = /* @__PURE__ */ __exportAll({ invoiceWebhook: () => invoiceWebhook });
6
+ /**
7
+ * Internal: endpoint called after the queue processes a subscription billing
8
+ */
9
+ const invoiceWebhook = defineNomkitMethod({
10
+ input: z$1.object({
11
+ billing: z$1.object({}),
12
+ signature: z$1.string().min(1).max(10),
13
+ status: z$1.enum([
14
+ "success",
15
+ "retrying",
16
+ "cancelled"
17
+ ]),
18
+ policy: z$1.string()
19
+ }),
20
+ route: {
21
+ path: "/invoice-webhook",
22
+ requireHeaders: true
23
+ }
24
+ }, async (ctx) => {
25
+ return { status: "event_recieved" };
26
+ });
27
+ //#endregion
28
+ export { api_exports, invoiceWebhook };