@lunora/payment 1.0.0-alpha.30 → 1.0.0-alpha.32
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.
- package/dist/index.mjs +1 -14
- package/dist/packem_shared/LunoraPaymentError-DWQ-v_NC.mjs +1 -0
- package/dist/packem_shared/MemoryPaymentStore-BICI1RdZ.mjs +1 -0
- package/dist/packem_shared/PAYMENT_TERMINAL_STATES-DVNbZxBy.mjs +1 -0
- package/dist/packem_shared/addMoney-BiiMn67T.mjs +1 -0
- package/dist/packem_shared/applyWebhookAction-BMVcvJ3t.mjs +1 -0
- package/dist/packem_shared/constantTimeEqual-BpVBvmN-.mjs +1 -0
- package/dist/packem_shared/createAdapterRegistry-CNIpj46I.mjs +1 -0
- package/dist/packem_shared/createDatabasePaymentStore-IG8Cb3_L.mjs +1 -0
- package/dist/packem_shared/createPayment-CW_iR6Hg.mjs +1 -0
- package/dist/packem_shared/entitlementsForReference-BbEOKu5d.mjs +1 -0
- package/dist/packem_shared/idempotencyKey-CfJKqcE3.mjs +1 -0
- package/dist/packem_shared/json-D85Nbd92.mjs +1 -0
- package/dist/packem_shared/lunoraDatabaseToPaymentDatabase-BFBlKLG3.mjs +1 -0
- package/dist/packem_shared/not-supported-DwGy4SQL.mjs +1 -0
- package/dist/packem_shared/observability-C9tz8mj_.mjs +1 -0
- package/dist/packem_shared/paymentTables-BMtUrXkM.mjs +1 -0
- package/dist/packem_shared/reconcile-DDdfivoy.mjs +1 -0
- package/dist/packem_shared/subscription-event-C-71ye9J.mjs +1 -0
- package/dist/providers/autumn-features.mjs +1 -95
- package/dist/providers/autumn.mjs +1 -275
- package/dist/providers/creem.mjs +1 -236
- package/dist/providers/dodopayments.mjs +1 -259
- package/dist/providers/polar.mjs +1 -231
- package/dist/providers/stripe.mjs +1 -271
- package/package.json +4 -4
- package/dist/packem_shared/LunoraPaymentError-BeQlhkZj.mjs +0 -19
- package/dist/packem_shared/MemoryPaymentStore-DvgdWa3C.mjs +0 -72
- package/dist/packem_shared/PAYMENT_TERMINAL_STATES-DrxV0clv.mjs +0 -26
- package/dist/packem_shared/addMoney-jSh_7TfF.mjs +0 -60
- package/dist/packem_shared/applyWebhookAction-CLAx4svt.mjs +0 -177
- package/dist/packem_shared/constantTimeEqual-BQjoW85H.mjs +0 -71
- package/dist/packem_shared/createAdapterRegistry-Ds7bx_TK.mjs +0 -24
- package/dist/packem_shared/createDatabasePaymentStore-C0NUCj1H.mjs +0 -172
- package/dist/packem_shared/createPayment-CKQAznOL.mjs +0 -200
- package/dist/packem_shared/entitlementsForReference-CzZGXPoZ.mjs +0 -51
- package/dist/packem_shared/idempotencyKey-BzaHGdHD.mjs +0 -9
- package/dist/packem_shared/json-D0Cb5aMv.mjs +0 -29
- package/dist/packem_shared/lunoraDatabaseToPaymentDatabase-DTsgPvfO.mjs +0 -29
- package/dist/packem_shared/not-supported-C0onRyia.mjs +0 -7
- package/dist/packem_shared/observability-CvhJ205g.mjs +0 -11
- package/dist/packem_shared/paymentTables-D4TbhaIk.mjs +0 -59
- package/dist/packem_shared/reconcile-qE3ERi3D.mjs +0 -85
- package/dist/packem_shared/subscription-event-CwEsWRCK.mjs +0 -17
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
import { toErrorBody } from '@lunora/errors';
|
|
2
|
-
import { usagePeriodStart, resolveEntitlements, featureNames, hasActivePrice } from './entitlementsForReference-CzZGXPoZ.mjs';
|
|
3
|
-
import { LunoraPaymentError } from './LunoraPaymentError-BeQlhkZj.mjs';
|
|
4
|
-
import { idempotencyKey, derivedIdempotencyKey } from './idempotencyKey-BzaHGdHD.mjs';
|
|
5
|
-
import { n as notifyObserver } from './observability-CvhJ205g.mjs';
|
|
6
|
-
import applyWebhookAction from './applyWebhookAction-CLAx4svt.mjs';
|
|
7
|
-
|
|
8
|
-
const jsonResponse = (body, status = 200, headers) => Response.json(body, { headers: { "content-type": "application/json", ...headers }, status });
|
|
9
|
-
|
|
10
|
-
const stripReferenceId = (metadata) => metadata && "referenceId" in metadata ? Object.fromEntries(Object.entries(metadata).filter(([key]) => key !== "referenceId")) : metadata;
|
|
11
|
-
const createPayment = (options) => {
|
|
12
|
-
const { adapter, store } = options;
|
|
13
|
-
const ensureAuthorized = async (referenceId) => {
|
|
14
|
-
if (!options.authorize) {
|
|
15
|
-
return;
|
|
16
|
-
}
|
|
17
|
-
let allowed;
|
|
18
|
-
try {
|
|
19
|
-
allowed = await options.authorize(referenceId);
|
|
20
|
-
} catch {
|
|
21
|
-
throw new LunoraPaymentError("FORBIDDEN", `caller not authorized for reference "${referenceId}"`);
|
|
22
|
-
}
|
|
23
|
-
if (!allowed) {
|
|
24
|
-
throw new LunoraPaymentError("FORBIDDEN", `caller not authorized for reference "${referenceId}"`);
|
|
25
|
-
}
|
|
26
|
-
};
|
|
27
|
-
const startCheckout = async (input) => {
|
|
28
|
-
await ensureAuthorized(input.referenceId);
|
|
29
|
-
const metadata = stripReferenceId(input.metadata);
|
|
30
|
-
let customerId;
|
|
31
|
-
const existing = await store.getCustomerByReference(adapter.identifier, input.referenceId);
|
|
32
|
-
if (existing) {
|
|
33
|
-
customerId = existing.id;
|
|
34
|
-
} else {
|
|
35
|
-
const customer = await adapter.getOrCreateCustomer({ email: input.email, referenceId: input.referenceId });
|
|
36
|
-
customerId = customer.id;
|
|
37
|
-
await store.upsertCustomer(customer);
|
|
38
|
-
}
|
|
39
|
-
const key = input.idempotencyKey ?? await derivedIdempotencyKey(
|
|
40
|
-
"checkout",
|
|
41
|
-
adapter.identifier,
|
|
42
|
-
input.referenceId,
|
|
43
|
-
input.priceId,
|
|
44
|
-
input.mode,
|
|
45
|
-
String(input.quantity ?? 1),
|
|
46
|
-
input.successUrl,
|
|
47
|
-
input.cancelUrl,
|
|
48
|
-
metadata ? JSON.stringify(metadata) : ""
|
|
49
|
-
);
|
|
50
|
-
return adapter.createCheckout({ ...input, customerId, idempotencyKey: key, metadata });
|
|
51
|
-
};
|
|
52
|
-
const evaluateFeature = async (entitlements, subscriptions, referenceId, featureId, need) => {
|
|
53
|
-
const limit = entitlements.limit(featureId);
|
|
54
|
-
if (limit !== void 0) {
|
|
55
|
-
const used = await store.sumUsage(referenceId, featureId, usagePeriodStart(subscriptions));
|
|
56
|
-
const balance = limit - used;
|
|
57
|
-
return { allowed: balance >= need, balance, limit, unlimited: false, used };
|
|
58
|
-
}
|
|
59
|
-
return { allowed: entitlements.has(featureId), unlimited: entitlements.has(featureId) };
|
|
60
|
-
};
|
|
61
|
-
return {
|
|
62
|
-
adapter,
|
|
63
|
-
attach: async (input) => startCheckout({ ...input, mode: input.mode ?? "subscription" }),
|
|
64
|
-
cancelSubscription: async (subscriptionId, cancelOptions) => {
|
|
65
|
-
const existing = await store.getSubscription(adapter.identifier, subscriptionId);
|
|
66
|
-
if (!existing) {
|
|
67
|
-
throw new LunoraPaymentError("NOT_FOUND", `subscription "${subscriptionId}" not found`);
|
|
68
|
-
}
|
|
69
|
-
try {
|
|
70
|
-
await ensureAuthorized(existing.referenceId);
|
|
71
|
-
} catch {
|
|
72
|
-
throw new LunoraPaymentError("NOT_FOUND", `subscription "${subscriptionId}" not found`);
|
|
73
|
-
}
|
|
74
|
-
const key = cancelOptions?.idempotencyKey ?? idempotencyKey("cancel_subscription", adapter.identifier, subscriptionId);
|
|
75
|
-
const updated = await adapter.cancelSubscription(subscriptionId, { ...cancelOptions, idempotencyKey: key });
|
|
76
|
-
await store.upsertSubscription(updated);
|
|
77
|
-
return updated;
|
|
78
|
-
},
|
|
79
|
-
check: async (input) => {
|
|
80
|
-
await ensureAuthorized(input.referenceId);
|
|
81
|
-
if (input.featureId === void 0 && input.priceId === void 0) {
|
|
82
|
-
throw new LunoraPaymentError("CONFIG_INVALID", "check() requires a featureId or priceId");
|
|
83
|
-
}
|
|
84
|
-
if (adapter.checkEntitlement) {
|
|
85
|
-
return adapter.checkEntitlement(input);
|
|
86
|
-
}
|
|
87
|
-
const subscriptions = await store.listSubscriptionsByReference(input.referenceId);
|
|
88
|
-
if (input.priceId !== void 0) {
|
|
89
|
-
return { allowed: hasActivePrice(subscriptions, input.priceId), unlimited: false };
|
|
90
|
-
}
|
|
91
|
-
if (input.featureId === void 0) {
|
|
92
|
-
throw new LunoraPaymentError("CONFIG_INVALID", "check() requires a featureId or priceId");
|
|
93
|
-
}
|
|
94
|
-
if (!options.entitlements) {
|
|
95
|
-
throw new LunoraPaymentError("CONFIG_INVALID", "check() requires `entitlements` to be configured");
|
|
96
|
-
}
|
|
97
|
-
const entitlements = resolveEntitlements(options.entitlements, subscriptions);
|
|
98
|
-
return evaluateFeature(entitlements, subscriptions, input.referenceId, input.featureId, input.quantity ?? 1);
|
|
99
|
-
},
|
|
100
|
-
createCheckout: async (input) => startCheckout(input),
|
|
101
|
-
createPortalSession: async (referenceId, returnUrl) => {
|
|
102
|
-
await ensureAuthorized(referenceId);
|
|
103
|
-
const customer = await store.getCustomerByReference(adapter.identifier, referenceId);
|
|
104
|
-
if (!customer) {
|
|
105
|
-
throw new LunoraPaymentError("NOT_FOUND", `no customer for reference "${referenceId}"`);
|
|
106
|
-
}
|
|
107
|
-
return adapter.createPortalSession({ customerId: customer.id, returnUrl });
|
|
108
|
-
},
|
|
109
|
-
handleWebhook: async (request) => {
|
|
110
|
-
let action;
|
|
111
|
-
try {
|
|
112
|
-
const payload = await request.text();
|
|
113
|
-
action = await adapter.parseWebhook({ headers: request.headers, payload });
|
|
114
|
-
} catch (error) {
|
|
115
|
-
if (error instanceof LunoraPaymentError) {
|
|
116
|
-
const { body, status } = toErrorBody(error);
|
|
117
|
-
return jsonResponse({ error: body.message }, status);
|
|
118
|
-
}
|
|
119
|
-
return jsonResponse({ error: "webhook error" }, 400);
|
|
120
|
-
}
|
|
121
|
-
const result = await applyWebhookAction(store, action, options.observability);
|
|
122
|
-
return jsonResponse({ applied: result.applied, reason: result.reason }, 200);
|
|
123
|
-
},
|
|
124
|
-
listBalances: async (referenceId) => {
|
|
125
|
-
await ensureAuthorized(referenceId);
|
|
126
|
-
if (adapter.getBalances) {
|
|
127
|
-
return adapter.getBalances(referenceId);
|
|
128
|
-
}
|
|
129
|
-
if (!options.entitlements) {
|
|
130
|
-
throw new LunoraPaymentError("CONFIG_INVALID", "listBalances() requires `entitlements` to be configured");
|
|
131
|
-
}
|
|
132
|
-
const subscriptions = await store.listSubscriptionsByReference(referenceId);
|
|
133
|
-
const entitlements = resolveEntitlements(options.entitlements, subscriptions);
|
|
134
|
-
return Promise.all(
|
|
135
|
-
featureNames(options.entitlements).map(async (featureId) => {
|
|
136
|
-
return {
|
|
137
|
-
featureId,
|
|
138
|
-
...await evaluateFeature(entitlements, subscriptions, referenceId, featureId, 1)
|
|
139
|
-
};
|
|
140
|
-
})
|
|
141
|
-
);
|
|
142
|
-
},
|
|
143
|
-
listSubscriptions: async (referenceId) => {
|
|
144
|
-
await ensureAuthorized(referenceId);
|
|
145
|
-
return store.listSubscriptionsByReference(referenceId);
|
|
146
|
-
},
|
|
147
|
-
store,
|
|
148
|
-
track: async (input) => {
|
|
149
|
-
await ensureAuthorized(input.referenceId);
|
|
150
|
-
const target = input.quantity ?? 1;
|
|
151
|
-
const key = input.idempotencyKey ?? crypto.randomUUID();
|
|
152
|
-
let delta = target;
|
|
153
|
-
if (input.mode === "set") {
|
|
154
|
-
const subscriptions = await store.listSubscriptionsByReference(input.referenceId);
|
|
155
|
-
const current = await store.sumUsage(input.referenceId, input.featureId, usagePeriodStart(subscriptions));
|
|
156
|
-
delta = target - current;
|
|
157
|
-
}
|
|
158
|
-
if (delta === 0) {
|
|
159
|
-
return { recorded: false, reportedToProvider: false };
|
|
160
|
-
}
|
|
161
|
-
const recorded = await store.recordUsage({
|
|
162
|
-
createdAt: Date.now(),
|
|
163
|
-
featureId: input.featureId,
|
|
164
|
-
idempotencyKey: key,
|
|
165
|
-
provider: adapter.identifier,
|
|
166
|
-
quantity: delta,
|
|
167
|
-
referenceId: input.referenceId,
|
|
168
|
-
reportedToProvider: false
|
|
169
|
-
});
|
|
170
|
-
if (!recorded) {
|
|
171
|
-
return { recorded: false, reportedToProvider: false };
|
|
172
|
-
}
|
|
173
|
-
if (delta <= 0 || !adapter.capabilities.usageMetering || !adapter.reportUsage) {
|
|
174
|
-
return { recorded: true, reportedToProvider: false };
|
|
175
|
-
}
|
|
176
|
-
try {
|
|
177
|
-
const customer = await store.getCustomerByReference(adapter.identifier, input.referenceId);
|
|
178
|
-
await adapter.reportUsage({
|
|
179
|
-
customerId: customer?.id,
|
|
180
|
-
featureId: input.featureId,
|
|
181
|
-
idempotencyKey: key,
|
|
182
|
-
quantity: delta,
|
|
183
|
-
referenceId: input.referenceId
|
|
184
|
-
});
|
|
185
|
-
await store.markUsageReported(adapter.identifier, key);
|
|
186
|
-
return { recorded: true, reportedToProvider: true };
|
|
187
|
-
} catch {
|
|
188
|
-
notifyObserver(options.observability, {
|
|
189
|
-
featureId: input.featureId,
|
|
190
|
-
provider: adapter.identifier,
|
|
191
|
-
referenceId: input.referenceId,
|
|
192
|
-
type: "usage.report_failed"
|
|
193
|
-
});
|
|
194
|
-
return { recorded: true, reportedToProvider: false };
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
};
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
export { createPayment };
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
const ACTIVE_STATES = /* @__PURE__ */ new Set(["active", "trialing"]);
|
|
2
|
-
const usagePeriodStart = (subscriptions) => {
|
|
3
|
-
let start = 0;
|
|
4
|
-
for (const subscription of subscriptions) {
|
|
5
|
-
if (ACTIVE_STATES.has(subscription.state) && subscription.currentPeriodStart !== void 0) {
|
|
6
|
-
start = Math.max(start, subscription.currentPeriodStart);
|
|
7
|
-
}
|
|
8
|
-
}
|
|
9
|
-
return start;
|
|
10
|
-
};
|
|
11
|
-
const featureNames = (config) => {
|
|
12
|
-
const names = /* @__PURE__ */ new Set();
|
|
13
|
-
for (const plan of Object.values(config.plans)) {
|
|
14
|
-
for (const feature of plan.features ?? []) {
|
|
15
|
-
names.add(feature);
|
|
16
|
-
}
|
|
17
|
-
for (const key of Object.keys(plan.limits ?? {})) {
|
|
18
|
-
names.add(key);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
return [...names].toSorted((a, b) => a.localeCompare(b));
|
|
22
|
-
};
|
|
23
|
-
const hasActivePrice = (subscriptions, priceId) => subscriptions.some((subscription) => subscription.priceId === priceId && ACTIVE_STATES.has(subscription.state));
|
|
24
|
-
const resolveEntitlements = (config, subscriptions) => {
|
|
25
|
-
const activePriceIds = new Set(subscriptions.filter((subscription) => ACTIVE_STATES.has(subscription.state)).map((subscription) => subscription.priceId));
|
|
26
|
-
const plans = [];
|
|
27
|
-
const features = /* @__PURE__ */ new Set();
|
|
28
|
-
const limits = /* @__PURE__ */ new Map();
|
|
29
|
-
for (const [name, plan] of Object.entries(config.plans)) {
|
|
30
|
-
if (!plan.priceIds.some((id) => activePriceIds.has(id))) {
|
|
31
|
-
continue;
|
|
32
|
-
}
|
|
33
|
-
plans.push(name);
|
|
34
|
-
for (const feature of plan.features ?? []) {
|
|
35
|
-
features.add(feature);
|
|
36
|
-
}
|
|
37
|
-
for (const [key, value] of Object.entries(plan.limits ?? {})) {
|
|
38
|
-
const current = limits.get(key);
|
|
39
|
-
limits.set(key, current === void 0 ? value : Math.max(current, value));
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
return {
|
|
43
|
-
features,
|
|
44
|
-
has: (feature) => features.has(feature),
|
|
45
|
-
limit: (key) => limits.get(key),
|
|
46
|
-
plans
|
|
47
|
-
};
|
|
48
|
-
};
|
|
49
|
-
const entitlementsForReference = async (store, config, referenceId) => resolveEntitlements(config, await store.listSubscriptionsByReference(referenceId));
|
|
50
|
-
|
|
51
|
-
export { entitlementsForReference, featureNames, hasActivePrice, resolveEntitlements, usagePeriodStart };
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
const encoder = new TextEncoder();
|
|
2
|
-
const sha256Hex = async (value) => [...new Uint8Array(await crypto.subtle.digest("SHA-256", encoder.encode(value)))].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
3
|
-
const idempotencyKey = (operation, ...parts) => [operation, ...parts.map(String)].join(":");
|
|
4
|
-
const derivedIdempotencyKey = async (operation, provider, ...parts) => {
|
|
5
|
-
const digest = await sha256Hex(JSON.stringify([provider, ...parts.map(String)]));
|
|
6
|
-
return `${operation}:${provider}:${digest}`;
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
export { derivedIdempotencyKey, idempotencyKey };
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
const asRecord = (value) => typeof value === "object" && value !== null ? value : {};
|
|
2
|
-
const readString = (object, key) => typeof object[key] === "string" ? object[key] : void 0;
|
|
3
|
-
const readNumber = (object, key) => typeof object[key] === "number" ? object[key] : void 0;
|
|
4
|
-
const readBoolean = (object, key) => typeof object[key] === "boolean" ? object[key] : void 0;
|
|
5
|
-
const readAny = (object, ...keys) => {
|
|
6
|
-
for (const key of keys) {
|
|
7
|
-
const value = readString(object, key);
|
|
8
|
-
if (value !== void 0) {
|
|
9
|
-
return value;
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
return void 0;
|
|
13
|
-
};
|
|
14
|
-
const readAnyNumber = (object, ...keys) => {
|
|
15
|
-
for (const key of keys) {
|
|
16
|
-
const value = readNumber(object, key);
|
|
17
|
-
if (value !== void 0) {
|
|
18
|
-
return value;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
return void 0;
|
|
22
|
-
};
|
|
23
|
-
const referenceFromMetadata = (object) => readString(asRecord(object.metadata), "referenceId");
|
|
24
|
-
const parseTimestamp = (value) => {
|
|
25
|
-
const parsed = typeof value === "string" ? Date.parse(value) : Number.NaN;
|
|
26
|
-
return Number.isNaN(parsed) ? void 0 : parsed;
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
export { asRecord as a, readNumber as b, readAny as c, readAnyNumber as d, readBoolean as e, referenceFromMetadata as f, parseTimestamp as p, readString as r };
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { createPayment } from './createPayment-CKQAznOL.mjs';
|
|
2
|
-
import { createDatabasePaymentStore } from './createDatabasePaymentStore-C0NUCj1H.mjs';
|
|
3
|
-
|
|
4
|
-
const lunoraDatabaseToPaymentDatabase = (database) => {
|
|
5
|
-
return {
|
|
6
|
-
delete: async (id) => database.delete(id),
|
|
7
|
-
findFirst: async (table, where) => await database.findFirst(table, { where }),
|
|
8
|
-
findMany: async (table, where) => {
|
|
9
|
-
const result = await database.findMany(table, { where });
|
|
10
|
-
return result.page;
|
|
11
|
-
},
|
|
12
|
-
insert: async (table, document) => database.insert(table, document),
|
|
13
|
-
patch: async (id, patch) => database.patch(id, patch)
|
|
14
|
-
};
|
|
15
|
-
};
|
|
16
|
-
const paymentsFromContext = (context, options) => {
|
|
17
|
-
const userId = context.auth?.userId ?? void 0;
|
|
18
|
-
return createPayment({
|
|
19
|
-
adapter: options.adapter,
|
|
20
|
-
// The default authorizer fails closed on an empty/whitespace reference: a missing identity or a
|
|
21
|
-
// blank reference (e.g. webhook-orphaned rows with `referenceId: ""`) is never authorized.
|
|
22
|
-
authorize: options.authorize ?? ((referenceId) => referenceId.trim() !== "" && userId !== void 0 && referenceId === userId),
|
|
23
|
-
entitlements: options.entitlements,
|
|
24
|
-
observability: options.observability,
|
|
25
|
-
store: createDatabasePaymentStore(lunoraDatabaseToPaymentDatabase(context.db))
|
|
26
|
-
});
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
export { lunoraDatabaseToPaymentDatabase, paymentsFromContext };
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import { defineTable } from '@lunora/server';
|
|
2
|
-
import { v } from '@lunora/values';
|
|
3
|
-
|
|
4
|
-
const customers = defineTable({
|
|
5
|
-
createdAt: v.number(),
|
|
6
|
-
email: v.optional(v.string()),
|
|
7
|
-
provider: v.string(),
|
|
8
|
-
providerCustomerId: v.string(),
|
|
9
|
-
referenceId: v.string()
|
|
10
|
-
}).index("by_provider_customer", ["provider", "providerCustomerId"], { unique: true }).index("by_reference", ["referenceId"]);
|
|
11
|
-
const subscriptions = defineTable({
|
|
12
|
-
cancelAtPeriodEnd: v.boolean(),
|
|
13
|
-
createdAt: v.number(),
|
|
14
|
-
currentPeriodEnd: v.optional(v.number()),
|
|
15
|
-
currentPeriodStart: v.optional(v.number()),
|
|
16
|
-
priceId: v.string(),
|
|
17
|
-
provider: v.string(),
|
|
18
|
-
providerSubscriptionId: v.string(),
|
|
19
|
-
quantity: v.number(),
|
|
20
|
-
referenceId: v.string(),
|
|
21
|
-
state: v.string(),
|
|
22
|
-
updatedAt: v.number()
|
|
23
|
-
}).index("by_provider_subscription", ["provider", "providerSubscriptionId"], { unique: true }).index("by_reference", ["referenceId"]);
|
|
24
|
-
const paymentSessions = defineTable({
|
|
25
|
-
amountMinor: v.bigint(),
|
|
26
|
-
capturedMinor: v.bigint(),
|
|
27
|
-
createdAt: v.number(),
|
|
28
|
-
currency: v.string(),
|
|
29
|
-
provider: v.string(),
|
|
30
|
-
providerSessionId: v.string(),
|
|
31
|
-
referenceId: v.string(),
|
|
32
|
-
refundedMinor: v.bigint(),
|
|
33
|
-
state: v.string(),
|
|
34
|
-
updatedAt: v.number()
|
|
35
|
-
}).index("by_provider_session", ["provider", "providerSessionId"], { unique: true }).index("by_reference", ["referenceId"]);
|
|
36
|
-
const events = defineTable({
|
|
37
|
-
processedAt: v.number(),
|
|
38
|
-
provider: v.string(),
|
|
39
|
-
providerEventId: v.string(),
|
|
40
|
-
type: v.string()
|
|
41
|
-
}).index("by_provider_event", ["provider", "providerEventId"], { unique: true });
|
|
42
|
-
const usageEvents = defineTable({
|
|
43
|
-
createdAt: v.number(),
|
|
44
|
-
featureId: v.string(),
|
|
45
|
-
idempotencyKey: v.string(),
|
|
46
|
-
provider: v.string(),
|
|
47
|
-
quantity: v.number(),
|
|
48
|
-
referenceId: v.string(),
|
|
49
|
-
reportedToProvider: v.boolean()
|
|
50
|
-
}).index("by_idempotency", ["provider", "idempotencyKey"], { unique: true }).index("by_reference_feature", ["referenceId", "featureId"]);
|
|
51
|
-
const paymentTables = {
|
|
52
|
-
customers,
|
|
53
|
-
events,
|
|
54
|
-
paymentSessions,
|
|
55
|
-
subscriptions,
|
|
56
|
-
usageEvents
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
export { paymentTables as default };
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import { compareMoney } from './addMoney-jSh_7TfF.mjs';
|
|
2
|
-
import { n as notifyObserver } from './observability-CvhJ205g.mjs';
|
|
3
|
-
|
|
4
|
-
const sameCurrencyAmount = (a, b) => a.currency === b.currency && compareMoney(a, b) === 0;
|
|
5
|
-
const subscriptionDrifted = (existing, current) => existing?.state !== current.state || existing.cancelAtPeriodEnd !== current.cancelAtPeriodEnd || existing.currentPeriodEnd !== current.currentPeriodEnd || existing.priceId !== current.priceId || existing.quantity !== current.quantity;
|
|
6
|
-
const paymentDrifted = (existing, current) => existing?.state !== current.state || !sameCurrencyAmount(existing.capturedAmount, current.capturedAmount) || !sameCurrencyAmount(existing.refundedAmount, current.refundedAmount);
|
|
7
|
-
const REFUND_STATES = /* @__PURE__ */ new Set(["partially_refunded", "refunded"]);
|
|
8
|
-
const mergePaymentTruth = (existing, current) => {
|
|
9
|
-
if (!existing) {
|
|
10
|
-
return current;
|
|
11
|
-
}
|
|
12
|
-
const sameRefundCurrency = existing.refundedAmount.currency === current.refundedAmount.currency;
|
|
13
|
-
const refundedAmount = sameRefundCurrency && compareMoney(existing.refundedAmount, current.refundedAmount) > 0 ? existing.refundedAmount : current.refundedAmount;
|
|
14
|
-
const state = current.state === "captured" && REFUND_STATES.has(existing.state) ? existing.state : current.state;
|
|
15
|
-
const referenceId = current.referenceId === "" ? existing.referenceId : current.referenceId;
|
|
16
|
-
return { ...current, referenceId, refundedAmount, state };
|
|
17
|
-
};
|
|
18
|
-
const reconcileSubscription = async (adapter, store, id, observer) => {
|
|
19
|
-
const current = await adapter.getSubscriptionStatus(id);
|
|
20
|
-
const existing = await store.getSubscription(adapter.identifier, id);
|
|
21
|
-
if (!subscriptionDrifted(existing, current)) {
|
|
22
|
-
return false;
|
|
23
|
-
}
|
|
24
|
-
await store.upsertSubscription({ ...current, createdAt: existing?.createdAt ?? current.createdAt });
|
|
25
|
-
notifyObserver(observer, { id, kind: "subscription", provider: adapter.identifier, type: "reconcile.drift" });
|
|
26
|
-
return true;
|
|
27
|
-
};
|
|
28
|
-
const reconcilePayment = async (adapter, store, id, observer) => {
|
|
29
|
-
const providerTruth = await adapter.getPaymentStatus(id);
|
|
30
|
-
const existing = await store.getPaymentSession(adapter.identifier, id);
|
|
31
|
-
const current = mergePaymentTruth(existing, providerTruth);
|
|
32
|
-
if (!paymentDrifted(existing, current)) {
|
|
33
|
-
return false;
|
|
34
|
-
}
|
|
35
|
-
await store.upsertPaymentSession({ ...current, createdAt: existing?.createdAt ?? current.createdAt });
|
|
36
|
-
notifyObserver(observer, { id, kind: "payment", provider: adapter.identifier, type: "reconcile.drift" });
|
|
37
|
-
return true;
|
|
38
|
-
};
|
|
39
|
-
const sweep = async (ids, kind, reconcileOne, adapter, observer) => {
|
|
40
|
-
const settled = await Promise.allSettled(ids.map((id) => reconcileOne(id)));
|
|
41
|
-
let updated = 0;
|
|
42
|
-
let failed = 0;
|
|
43
|
-
for (const [index, result] of settled.entries()) {
|
|
44
|
-
if (result.status === "fulfilled") {
|
|
45
|
-
if (result.value) {
|
|
46
|
-
updated += 1;
|
|
47
|
-
}
|
|
48
|
-
} else {
|
|
49
|
-
failed += 1;
|
|
50
|
-
notifyObserver(observer, { error: result.reason, id: ids[index] ?? "", kind, provider: adapter.identifier, type: "reconcile.error" });
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
return { failed, updated };
|
|
54
|
-
};
|
|
55
|
-
const reconcile = async (input) => {
|
|
56
|
-
const { adapter, observability, store } = input;
|
|
57
|
-
const subscriptionIds = input.subscriptionIds ?? [];
|
|
58
|
-
const paymentSessionIds = input.paymentSessionIds ?? [];
|
|
59
|
-
const subscriptionCounts = await sweep(
|
|
60
|
-
subscriptionIds,
|
|
61
|
-
"subscription",
|
|
62
|
-
(id) => reconcileSubscription(adapter, store, id, observability),
|
|
63
|
-
adapter,
|
|
64
|
-
observability
|
|
65
|
-
);
|
|
66
|
-
const paymentCounts = await sweep(paymentSessionIds, "payment", (id) => reconcilePayment(adapter, store, id, observability), adapter, observability);
|
|
67
|
-
notifyObserver(observability, {
|
|
68
|
-
failedPayments: paymentCounts.failed,
|
|
69
|
-
failedSubscriptions: subscriptionCounts.failed,
|
|
70
|
-
provider: adapter.identifier,
|
|
71
|
-
type: "reconcile.completed",
|
|
72
|
-
updatedPayments: paymentCounts.updated,
|
|
73
|
-
updatedSubscriptions: subscriptionCounts.updated
|
|
74
|
-
});
|
|
75
|
-
return {
|
|
76
|
-
checkedPayments: paymentSessionIds.length,
|
|
77
|
-
checkedSubscriptions: subscriptionIds.length,
|
|
78
|
-
failedPayments: paymentCounts.failed,
|
|
79
|
-
failedSubscriptions: subscriptionCounts.failed,
|
|
80
|
-
updatedPayments: paymentCounts.updated,
|
|
81
|
-
updatedSubscriptions: subscriptionCounts.updated
|
|
82
|
-
};
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
export { reconcile };
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
const stateToEventType = (state) => {
|
|
2
|
-
if (state === "canceled") {
|
|
3
|
-
return "subscription.canceled";
|
|
4
|
-
}
|
|
5
|
-
if (state === "past_due") {
|
|
6
|
-
return "subscription.past_due";
|
|
7
|
-
}
|
|
8
|
-
if (state === "paused") {
|
|
9
|
-
return "subscription.paused";
|
|
10
|
-
}
|
|
11
|
-
if (state === "active" || state === "trialing") {
|
|
12
|
-
return "subscription.active";
|
|
13
|
-
}
|
|
14
|
-
return "subscription.updated";
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
export { stateToEventType as s };
|