@lunora/payment 0.0.0 → 1.0.0-alpha.10

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.
@@ -0,0 +1,51 @@
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 };
@@ -0,0 +1,3 @@
1
+ const idempotencyKey = (operation, ...parts) => [operation, ...parts.map(String)].join(":");
2
+
3
+ export { idempotencyKey as default };
@@ -0,0 +1,6 @@
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
+
6
+ export { asRecord as a, readBoolean as b, readNumber as c, readString as r };
@@ -0,0 +1,29 @@
1
+ import { createPayment } from './createPayment-BccfPGyw.mjs';
2
+ import { createDatabasePaymentStore } from './createDatabasePaymentStore-bYB_HUE6.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 };
@@ -0,0 +1,11 @@
1
+ const notifyObserver = (observer, event) => {
2
+ if (!observer) {
3
+ return;
4
+ }
5
+ try {
6
+ observer(event);
7
+ } catch {
8
+ }
9
+ };
10
+
11
+ export { notifyObserver as n };
@@ -0,0 +1,127 @@
1
+ import { defineTable } from '@lunora/server';
2
+ import { v } from '@lunora/values';
3
+
4
+ const products = defineTable({
5
+ description: v.optional(v.string()),
6
+ name: v.string(),
7
+ provider: v.string(),
8
+ providerProductId: v.string()
9
+ }).index("by_provider_product", ["provider", "providerProductId"], { unique: true });
10
+ const prices = defineTable({
11
+ active: v.boolean(),
12
+ amountMinor: v.bigint(),
13
+ currency: v.string(),
14
+ interval: v.optional(v.string()),
15
+ provider: v.string(),
16
+ providerPriceId: v.string(),
17
+ providerProductId: v.string()
18
+ }).index("by_provider_price", ["provider", "providerPriceId"], { unique: true });
19
+ const customers = defineTable({
20
+ createdAt: v.number(),
21
+ email: v.optional(v.string()),
22
+ provider: v.string(),
23
+ providerCustomerId: v.string(),
24
+ referenceId: v.string()
25
+ }).index("by_provider_customer", ["provider", "providerCustomerId"], { unique: true }).index("by_reference", ["referenceId"]);
26
+ const subscriptions = defineTable({
27
+ cancelAtPeriodEnd: v.boolean(),
28
+ createdAt: v.number(),
29
+ currentPeriodEnd: v.optional(v.number()),
30
+ currentPeriodStart: v.optional(v.number()),
31
+ priceId: v.string(),
32
+ provider: v.string(),
33
+ providerSubscriptionId: v.string(),
34
+ quantity: v.number(),
35
+ referenceId: v.string(),
36
+ state: v.string(),
37
+ updatedAt: v.number()
38
+ }).index("by_provider_subscription", ["provider", "providerSubscriptionId"], { unique: true }).index("by_reference", ["referenceId"]);
39
+ const checkouts = defineTable({
40
+ createdAt: v.number(),
41
+ mode: v.string(),
42
+ priceId: v.string(),
43
+ provider: v.string(),
44
+ providerCheckoutId: v.string(),
45
+ referenceId: v.string(),
46
+ url: v.string()
47
+ }).index("by_provider_checkout", ["provider", "providerCheckoutId"], { unique: true });
48
+ const paymentSessions = defineTable({
49
+ amountMinor: v.bigint(),
50
+ capturedMinor: v.bigint(),
51
+ createdAt: v.number(),
52
+ currency: v.string(),
53
+ provider: v.string(),
54
+ providerSessionId: v.string(),
55
+ referenceId: v.string(),
56
+ refundedMinor: v.bigint(),
57
+ state: v.string(),
58
+ updatedAt: v.number()
59
+ }).index("by_provider_session", ["provider", "providerSessionId"], { unique: true }).index("by_reference", ["referenceId"]);
60
+ const payments = defineTable({
61
+ amountMinor: v.bigint(),
62
+ createdAt: v.number(),
63
+ currency: v.string(),
64
+ provider: v.string(),
65
+ providerPaymentId: v.string(),
66
+ referenceId: v.string(),
67
+ sessionId: v.string(),
68
+ status: v.string()
69
+ }).index("by_provider_payment", ["provider", "providerPaymentId"], { unique: true });
70
+ const captures = defineTable({
71
+ amountMinor: v.bigint(),
72
+ createdAt: v.number(),
73
+ currency: v.string(),
74
+ provider: v.string(),
75
+ providerCaptureId: v.string(),
76
+ sessionId: v.string()
77
+ }).index("by_session", ["sessionId"]);
78
+ const refunds = defineTable({
79
+ amountMinor: v.bigint(),
80
+ createdAt: v.number(),
81
+ currency: v.string(),
82
+ provider: v.string(),
83
+ providerRefundId: v.string(),
84
+ reason: v.optional(v.string()),
85
+ sessionId: v.string()
86
+ }).index("by_session", ["sessionId"]);
87
+ const invoices = defineTable({
88
+ amountMinor: v.bigint(),
89
+ createdAt: v.number(),
90
+ currency: v.string(),
91
+ provider: v.string(),
92
+ providerInvoiceId: v.string(),
93
+ referenceId: v.string(),
94
+ status: v.string(),
95
+ subscriptionId: v.optional(v.string())
96
+ }).index("by_provider_invoice", ["provider", "providerInvoiceId"], { unique: true });
97
+ const events = defineTable({
98
+ processedAt: v.number(),
99
+ provider: v.string(),
100
+ providerEventId: v.string(),
101
+ type: v.string()
102
+ }).index("by_provider_event", ["provider", "providerEventId"], { unique: true });
103
+ const usageEvents = defineTable({
104
+ createdAt: v.number(),
105
+ featureId: v.string(),
106
+ idempotencyKey: v.string(),
107
+ provider: v.string(),
108
+ quantity: v.number(),
109
+ referenceId: v.string(),
110
+ reportedToProvider: v.boolean()
111
+ }).index("by_idempotency", ["provider", "idempotencyKey"], { unique: true }).index("by_reference_feature", ["referenceId", "featureId"]);
112
+ const paymentTables = {
113
+ captures,
114
+ checkouts,
115
+ customers,
116
+ events,
117
+ invoices,
118
+ paymentSessions,
119
+ payments,
120
+ prices,
121
+ products,
122
+ refunds,
123
+ subscriptions,
124
+ usageEvents
125
+ };
126
+
127
+ export { paymentTables as default };
@@ -0,0 +1,73 @@
1
+ import { compareMoney } from './addMoney-bCcs1nyw.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 reconcileSubscription = async (adapter, store, id, observer) => {
8
+ const current = await adapter.getSubscriptionStatus(id);
9
+ const existing = await store.getSubscription(adapter.identifier, id);
10
+ if (!subscriptionDrifted(existing, current)) {
11
+ return false;
12
+ }
13
+ await store.upsertSubscription({ ...current, createdAt: existing?.createdAt ?? current.createdAt });
14
+ notifyObserver(observer, { id, kind: "subscription", provider: adapter.identifier, type: "reconcile.drift" });
15
+ return true;
16
+ };
17
+ const reconcilePayment = async (adapter, store, id, observer) => {
18
+ const current = await adapter.getPaymentStatus(id);
19
+ const existing = await store.getPaymentSession(adapter.identifier, id);
20
+ if (!paymentDrifted(existing, current)) {
21
+ return false;
22
+ }
23
+ await store.upsertPaymentSession({ ...current, createdAt: existing?.createdAt ?? current.createdAt });
24
+ notifyObserver(observer, { id, kind: "payment", provider: adapter.identifier, type: "reconcile.drift" });
25
+ return true;
26
+ };
27
+ const sweep = async (ids, kind, reconcileOne, adapter, observer) => {
28
+ const settled = await Promise.allSettled(ids.map((id) => reconcileOne(id)));
29
+ let updated = 0;
30
+ let failed = 0;
31
+ for (const [index, result] of settled.entries()) {
32
+ if (result.status === "fulfilled") {
33
+ if (result.value) {
34
+ updated += 1;
35
+ }
36
+ } else {
37
+ failed += 1;
38
+ notifyObserver(observer, { error: result.reason, id: ids[index] ?? "", kind, provider: adapter.identifier, type: "reconcile.error" });
39
+ }
40
+ }
41
+ return { failed, updated };
42
+ };
43
+ const reconcile = async (input) => {
44
+ const { adapter, observability, store } = input;
45
+ const subscriptionIds = input.subscriptionIds ?? [];
46
+ const paymentSessionIds = input.paymentSessionIds ?? [];
47
+ const subscriptionCounts = await sweep(
48
+ subscriptionIds,
49
+ "subscription",
50
+ (id) => reconcileSubscription(adapter, store, id, observability),
51
+ adapter,
52
+ observability
53
+ );
54
+ const paymentCounts = await sweep(paymentSessionIds, "payment", (id) => reconcilePayment(adapter, store, id, observability), adapter, observability);
55
+ notifyObserver(observability, {
56
+ failedPayments: paymentCounts.failed,
57
+ failedSubscriptions: subscriptionCounts.failed,
58
+ provider: adapter.identifier,
59
+ type: "reconcile.completed",
60
+ updatedPayments: paymentCounts.updated,
61
+ updatedSubscriptions: subscriptionCounts.updated
62
+ });
63
+ return {
64
+ checkedPayments: paymentSessionIds.length,
65
+ checkedSubscriptions: subscriptionIds.length,
66
+ failedPayments: paymentCounts.failed,
67
+ failedSubscriptions: subscriptionCounts.failed,
68
+ updatedPayments: paymentCounts.updated,
69
+ updatedSubscriptions: subscriptionCounts.updated
70
+ };
71
+ };
72
+
73
+ export { reconcile };
package/package.json CHANGED
@@ -1,32 +1,65 @@
1
1
  {
2
2
  "name": "@lunora/payment",
3
- "version": "0.0.0",
3
+ "version": "1.0.0-alpha.10",
4
4
  "description": "Provider-agnostic payments for Lunora: Stripe-first adapter, webhook sync, and subscription/payment state machine",
5
- "license": "FSL-1.1-Apache-2.0",
6
- "homepage": "https://lunora.sh",
7
- "repository": {
8
- "type": "git",
9
- "url": "git+https://github.com/anolilab/lunora.git",
10
- "directory": "packages/payment"
11
- },
12
- "bugs": {
13
- "url": "https://github.com/anolilab/lunora/issues"
14
- },
15
5
  "keywords": [
16
- "lunora",
6
+ "billing",
17
7
  "cloudflare",
18
- "workers",
19
8
  "durable-objects",
9
+ "lunora",
20
10
  "payment",
21
- "billing",
22
11
  "stripe",
23
12
  "subscriptions",
24
- "webhooks"
13
+ "webhooks",
14
+ "workers"
25
15
  ],
16
+ "homepage": "https://lunora.sh",
17
+ "bugs": "https://github.com/anolilab/lunora/issues",
18
+ "license": "FSL-1.1-Apache-2.0",
19
+ "author": {
20
+ "name": "Daniel Bannert",
21
+ "email": "d.bannert@anolilab.de"
22
+ },
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/anolilab/lunora.git",
26
+ "directory": "packages/payment"
27
+ },
28
+ "files": [
29
+ "./dist",
30
+ "__assets__",
31
+ "README.md",
32
+ "LICENSE.md"
33
+ ],
34
+ "type": "module",
35
+ "sideEffects": false,
36
+ "main": "./dist/index.mjs",
37
+ "module": "./dist/index.mjs",
38
+ "types": "./dist/index.d.ts",
39
+ "exports": {
40
+ ".": {
41
+ "types": "./dist/index.d.ts",
42
+ "import": "./dist/index.mjs"
43
+ },
44
+ "./package.json": "./package.json"
45
+ },
26
46
  "publishConfig": {
27
47
  "access": "public"
28
48
  },
29
- "files": [
30
- "README.md"
31
- ]
49
+ "dependencies": {
50
+ "@lunora/server": "1.0.0-alpha.10",
51
+ "@lunora/values": "1.0.0-alpha.3",
52
+ "dinero.js": "2.0.2"
53
+ },
54
+ "peerDependencies": {
55
+ "stripe": "^19.0.0"
56
+ },
57
+ "peerDependenciesMeta": {
58
+ "stripe": {
59
+ "optional": true
60
+ }
61
+ },
62
+ "engines": {
63
+ "node": "^22.15.0 || >=24.11.0"
64
+ }
32
65
  }