@odla-ai/chapter 0.48.3 → 0.49.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.
- package/dist/{chunk-KZ2AXEZW.js → chunk-KHDO2QFA.js} +749 -543
- package/dist/chunk-KHDO2QFA.js.map +1 -0
- package/dist/{chunk-MQ4OPZCS.js → chunk-KXIGZWG5.js} +720 -615
- package/dist/chunk-KXIGZWG5.js.map +1 -0
- package/dist/{chunk-4HS2JPE2.js → chunk-N2JIKINY.js} +17 -1
- package/dist/chunk-N2JIKINY.js.map +1 -0
- package/dist/{copy-context-CMupQL9h.d.ts → copy-context-By4Nq7yB.d.ts} +6 -1
- package/dist/index.cjs +488 -118
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +50 -3
- package/dist/index.d.ts +50 -3
- package/dist/index.js +484 -114
- package/dist/index.js.map +1 -1
- package/dist/ui/admin/index.d.ts +14 -3
- package/dist/ui/admin/index.js +6 -2
- package/dist/ui/index.d.ts +3 -3
- package/dist/ui/index.js +11 -3
- package/dist/ui/member/index.d.ts +36 -4
- package/dist/ui/member/index.js +6 -2
- package/dist/worker/index.cjs +7109 -6318
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +24 -1
- package/dist/worker/index.d.ts +24 -1
- package/dist/worker/index.js +7239 -6448
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-4HS2JPE2.js.map +0 -1
- package/dist/chunk-KZ2AXEZW.js.map +0 -1
- package/dist/chunk-MQ4OPZCS.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -38,12 +38,12 @@ function resolveApplication(a) {
|
|
|
38
38
|
if (!Array.isArray(arr) || !arr.every((f) => typeof f === "string" && f !== "")) {
|
|
39
39
|
throw new Error(`defineChapter.application.${name}: must be an array of field-name strings`);
|
|
40
40
|
}
|
|
41
|
-
const unsafe = arr.find((
|
|
41
|
+
const unsafe = arr.find((field2) => !SCHEMA_FIELD.test(field2));
|
|
42
42
|
if (unsafe) {
|
|
43
43
|
throw new Error(`defineChapter.application.${name}: field "${unsafe}" must match ${SCHEMA_FIELD.source}`);
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
|
-
const duplicate = [...required, ...optional].find((
|
|
46
|
+
const duplicate = [...required, ...optional].find((field2, index2, fields) => fields.indexOf(field2) !== index2);
|
|
47
47
|
if (duplicate) {
|
|
48
48
|
throw new Error(`defineChapter.application: duplicate field "${duplicate}" across required/optional lists`);
|
|
49
49
|
}
|
|
@@ -54,14 +54,14 @@ function resolveApplication(a) {
|
|
|
54
54
|
throw new Error("defineChapter.application.crmFields: must be an array of field-name strings");
|
|
55
55
|
}
|
|
56
56
|
const conditions = a?.conditions ?? {};
|
|
57
|
-
for (const [
|
|
57
|
+
for (const [field2, declared] of Object.entries(conditions)) {
|
|
58
58
|
for (const key3 of ["visibleWhen", "requiredWhen"]) {
|
|
59
59
|
const expression = declared?.[key3];
|
|
60
60
|
if (expression === void 0) continue;
|
|
61
61
|
if (typeof expression !== "string" || !expression.trim()) {
|
|
62
|
-
throw new Error(`defineChapter.application.conditions.${
|
|
62
|
+
throw new Error(`defineChapter.application.conditions.${field2}.${key3}: must be a non-empty CEL condition`);
|
|
63
63
|
}
|
|
64
|
-
assertFieldCondition(expression, `defineChapter.application.conditions.${
|
|
64
|
+
assertFieldCondition(expression, `defineChapter.application.conditions.${field2}.${key3}`);
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
return {
|
|
@@ -132,6 +132,13 @@ async function submitApplication(db, chapter, fields, opts) {
|
|
|
132
132
|
if (opts.groupId) row2.groupId = opts.groupId;
|
|
133
133
|
if (opts.runtime) row2.stripeRuntime = opts.runtime;
|
|
134
134
|
if (chapter.account !== "none") row2.clerkAccountPending = true;
|
|
135
|
+
if (opts.namedSeat) Object.assign(row2, {
|
|
136
|
+
namedSeatId: opts.namedSeat.seatId,
|
|
137
|
+
namedSeatPrimaryApplicationId: opts.namedSeat.primaryApplicationId,
|
|
138
|
+
namedSeatClaimPending: true,
|
|
139
|
+
clerkUserId: opts.namedSeat.userId,
|
|
140
|
+
clerkAccountPending: false
|
|
141
|
+
});
|
|
135
142
|
if (typeof fields.email === "string" && isValidEmail(fields.email.trim())) {
|
|
136
143
|
row2.emailNormalized = normalizeChapterEmail(fields.email);
|
|
137
144
|
row2.signupEmailKey = row2.emailNormalized;
|
|
@@ -261,6 +268,10 @@ var applicationAttrs = {
|
|
|
261
268
|
giftPaidAt: attr("number", { optional: true, indexed: true }),
|
|
262
269
|
giftRevokedAt: attr("number", { optional: true, indexed: true }),
|
|
263
270
|
giftCheckoutStartedAt: attr("number", { optional: true, indexed: true }),
|
|
271
|
+
membershipRestartId: attr("string", { optional: true, indexed: true }),
|
|
272
|
+
membershipStatus: attr("string", { optional: true }),
|
|
273
|
+
membershipGraceEndsAt: attr("number", { optional: true }),
|
|
274
|
+
membershipAutoRenew: attr("boolean", { optional: true }),
|
|
264
275
|
namedSeatId: attr("string", { optional: true, unique: true, indexed: true }),
|
|
265
276
|
namedSeatPrimaryApplicationId: attr("string", { optional: true }),
|
|
266
277
|
namedSeatClaimPending: attr("boolean", { optional: true, indexed: true }),
|
|
@@ -271,19 +282,19 @@ function applicationsFor(application) {
|
|
|
271
282
|
for (const [name, spec] of Object.entries(applicationAttrs)) {
|
|
272
283
|
attrs[name] = { ...spec, optional: APPLICATION_INPUT_FIELDS.has(name) ? true : spec.optional };
|
|
273
284
|
}
|
|
274
|
-
const apply = (
|
|
275
|
-
const existing = Object.hasOwn(attrs,
|
|
276
|
-
if (existing && !APPLICATION_INPUT_FIELDS.has(
|
|
277
|
-
if (existing && existing.type !== "string") throw new Error(`defineChapter.application: field "${
|
|
278
|
-
Object.defineProperty(attrs,
|
|
285
|
+
const apply = (field2, optional) => {
|
|
286
|
+
const existing = Object.hasOwn(attrs, field2) ? attrs[field2] : void 0;
|
|
287
|
+
if (existing && !APPLICATION_INPUT_FIELDS.has(field2)) throw new Error(`defineChapter.application: field "${field2}" is reserved by the applications schema`);
|
|
288
|
+
if (existing && existing.type !== "string") throw new Error(`defineChapter.application: field "${field2}" is not a configurable string field`);
|
|
289
|
+
Object.defineProperty(attrs, field2, {
|
|
279
290
|
value: existing ? { ...existing, optional } : attr("string", { optional }),
|
|
280
291
|
enumerable: true,
|
|
281
292
|
configurable: true,
|
|
282
293
|
writable: true
|
|
283
294
|
});
|
|
284
295
|
};
|
|
285
|
-
for (const
|
|
286
|
-
for (const
|
|
296
|
+
for (const field2 of application.required) apply(field2, false);
|
|
297
|
+
for (const field2 of application.optional) apply(field2, true);
|
|
287
298
|
return { attrs };
|
|
288
299
|
}
|
|
289
300
|
|
|
@@ -490,6 +501,19 @@ var giftRedemptionAttemptsEntity = { attrs: {
|
|
|
490
501
|
updatedAt: attr4("number")
|
|
491
502
|
} };
|
|
492
503
|
|
|
504
|
+
// src/payment-recovery-schema.ts
|
|
505
|
+
var field = (type, unique = false) => ({ type, unique, indexed: type !== "json", optional: false });
|
|
506
|
+
var paymentRecoveryReviewsEntity = { attrs: {
|
|
507
|
+
id: field("string", true),
|
|
508
|
+
eventId: field("string"),
|
|
509
|
+
actorUserId: field("string"),
|
|
510
|
+
reviewedAt: field("number"),
|
|
511
|
+
previousDigest: field("string"),
|
|
512
|
+
providerDigest: field("string"),
|
|
513
|
+
scope: field("string"),
|
|
514
|
+
previousState: field("json")
|
|
515
|
+
} };
|
|
516
|
+
|
|
493
517
|
// src/schema-stripe.ts
|
|
494
518
|
var attr5 = (type, flags = {}) => ({
|
|
495
519
|
type,
|
|
@@ -510,6 +534,10 @@ var stripeEventReceiptsEntity = {
|
|
|
510
534
|
dataEnvironment: attr5("string", { indexed: true, optional: true }),
|
|
511
535
|
runtime: attr5("string", { indexed: true, optional: true }),
|
|
512
536
|
chapterId: attr5("string", { indexed: true, optional: true }),
|
|
537
|
+
retryReady: attr5("boolean", { optional: true }),
|
|
538
|
+
reviewedDigest: attr5("string", { optional: true }),
|
|
539
|
+
reviewId: attr5("string", { optional: true }),
|
|
540
|
+
collisionDigest: attr5("string", { optional: true }),
|
|
513
541
|
receivedAt: attr5("number", { indexed: true })
|
|
514
542
|
}
|
|
515
543
|
};
|
|
@@ -534,6 +562,7 @@ var subscriptionCheckoutIntentsEntity = {
|
|
|
534
562
|
requestedCustomerId: attr5("string", { indexed: true, optional: true }),
|
|
535
563
|
providerCustomerId: attr5("string", { indexed: true, optional: true }),
|
|
536
564
|
providerSubscriptionId: attr5("string", { unique: true, indexed: true, optional: true }),
|
|
565
|
+
purchaseAttemptId: attr5("string", { optional: true }),
|
|
537
566
|
status: attr5("string", { indexed: true }),
|
|
538
567
|
createdAt: attr5("number", { indexed: true }),
|
|
539
568
|
updatedAt: attr5("number", { indexed: true })
|
|
@@ -558,6 +587,20 @@ var membershipQuoteProjectionsEntity = {
|
|
|
558
587
|
quote: attr5("json", { optional: true })
|
|
559
588
|
}
|
|
560
589
|
};
|
|
590
|
+
var membershipCheckoutHistoryEntity = { attrs: {
|
|
591
|
+
id: attr5("string", { unique: true, indexed: true }),
|
|
592
|
+
applicationId: attr5("string", { indexed: true }),
|
|
593
|
+
appId: attr5("string"),
|
|
594
|
+
chapterId: attr5("string"),
|
|
595
|
+
environment: attr5("string"),
|
|
596
|
+
runtime: attr5("string"),
|
|
597
|
+
skuId: attr5("string"),
|
|
598
|
+
providerSubscriptionId: attr5("string", { indexed: true }),
|
|
599
|
+
applicationSnapshot: attr5("json"),
|
|
600
|
+
intentSnapshot: attr5("json"),
|
|
601
|
+
quoteSnapshot: attr5("json"),
|
|
602
|
+
archivedAt: attr5("number")
|
|
603
|
+
} };
|
|
561
604
|
|
|
562
605
|
// src/schema-network-person.ts
|
|
563
606
|
var string = (flags = {}) => ({
|
|
@@ -691,15 +734,19 @@ var exceptions = { attrs: {
|
|
|
691
734
|
kind: string2({ indexed: true }),
|
|
692
735
|
sourceKey: string2({ indexed: true, optional: true }),
|
|
693
736
|
status: string2({ indexed: true }),
|
|
737
|
+
resolvedDigest: string2({ optional: true }),
|
|
738
|
+
incomingDigest: string2({ optional: true }),
|
|
694
739
|
detail: json2(),
|
|
695
740
|
updatedAt: number2({ indexed: true })
|
|
696
741
|
} };
|
|
697
742
|
var networkCommercialEntities = {
|
|
743
|
+
paymentRecoveryReviews: paymentRecoveryReviewsEntity,
|
|
698
744
|
networkCommercialEvents: events,
|
|
699
745
|
networkCommercialCustomers: projection(),
|
|
700
746
|
networkCommercialSubscriptions: projection(),
|
|
701
747
|
networkCommercialInvoices: projection(),
|
|
702
748
|
networkCommercialRefunds: projection(),
|
|
749
|
+
networkCommercialSeats: projection(),
|
|
703
750
|
networkCommercialExceptions: exceptions
|
|
704
751
|
};
|
|
705
752
|
|
|
@@ -902,8 +949,9 @@ function chapterDb(mode, auth, includeNetworkNotes = false, application = resolv
|
|
|
902
949
|
entities.tiers = tiers;
|
|
903
950
|
entities.meetings = meetings;
|
|
904
951
|
entities.emailLog = emailLog;
|
|
952
|
+
entities.paymentRecoveryReviews = paymentRecoveryReviewsEntity;
|
|
905
953
|
entities.stripeEventReceipts = stripeEventReceiptsEntity;
|
|
906
|
-
entities
|
|
954
|
+
Object.assign(entities, { subscriptionCheckoutIntents: subscriptionCheckoutIntentsEntity, membershipCheckoutHistory: membershipCheckoutHistoryEntity });
|
|
907
955
|
entities.membershipQuoteProjections = membershipQuoteProjectionsEntity;
|
|
908
956
|
entities.namedSeatConsents = namedSeatConsentsEntity;
|
|
909
957
|
entities.admissionGrants = admissionGrantsEntity;
|
|
@@ -1441,6 +1489,19 @@ var DEFAULT_COMMON_COPY = {
|
|
|
1441
1489
|
unknown: "(unknown)"
|
|
1442
1490
|
};
|
|
1443
1491
|
var DEFAULT_JOIN_COPY = {
|
|
1492
|
+
namedSeat: {
|
|
1493
|
+
title: "Your membership invitation",
|
|
1494
|
+
from: "{purchaserName} invited you.",
|
|
1495
|
+
prepaid: "Your {tierName} seat is already paid for.",
|
|
1496
|
+
body: "Accept your invitation, then complete your own application requirements. You will not be asked to pay for this seat.",
|
|
1497
|
+
consent: "I accept this membership for myself. It cannot be transferred and remains current while the purchaser's membership and my seat are current.",
|
|
1498
|
+
accept: "Accept my invitation",
|
|
1499
|
+
accepting: "Confirming your invitation\u2026",
|
|
1500
|
+
loading: "Loading your invitation\u2026",
|
|
1501
|
+
unavailable: "Your invitation is unavailable. Sign in with the invited email address, then try again.",
|
|
1502
|
+
retry: "Try again",
|
|
1503
|
+
failure: "We could not confirm acceptance. Retry this invitation; you do not need to purchase another membership."
|
|
1504
|
+
},
|
|
1444
1505
|
form: {
|
|
1445
1506
|
submit: "Submit application",
|
|
1446
1507
|
submitting: "Submitting\u2026",
|
|
@@ -1448,6 +1509,8 @@ var DEFAULT_JOIN_COPY = {
|
|
|
1448
1509
|
unexpectedFailure: "Something went wrong. Please try again."
|
|
1449
1510
|
},
|
|
1450
1511
|
booking: {
|
|
1512
|
+
title: "Book your introduction",
|
|
1513
|
+
intro: "Choose a time to meet your chapter and get to know each other.",
|
|
1451
1514
|
unavailable: "Scheduling is briefly unavailable. We'll reach out by email to arrange your call.",
|
|
1452
1515
|
loadFailed: "Times are briefly unavailable. Please try again.",
|
|
1453
1516
|
slotTaken: "That time was just taken. Here are the current openings.",
|
|
@@ -1639,16 +1702,16 @@ function resolveFormationFlowFields(crm, formation) {
|
|
|
1639
1702
|
if (!formation.enabled) return [];
|
|
1640
1703
|
const def = crm.type(formation.type);
|
|
1641
1704
|
return [...formation.required, ...formation.optional].map((id10, order2) => {
|
|
1642
|
-
const
|
|
1705
|
+
const field2 = def.fields[id10];
|
|
1643
1706
|
return {
|
|
1644
1707
|
id: id10,
|
|
1645
|
-
label:
|
|
1646
|
-
type:
|
|
1708
|
+
label: field2.label ?? humanize(id10),
|
|
1709
|
+
type: field2.type,
|
|
1647
1710
|
required: formation.required.includes(id10),
|
|
1648
1711
|
order: order2,
|
|
1649
1712
|
maxLength: formation.maxLen[id10] ?? formation.defaultMaxLen,
|
|
1650
|
-
...
|
|
1651
|
-
...id10 === def.emailField ||
|
|
1713
|
+
...field2.options ? { options: [...field2.options] } : {},
|
|
1714
|
+
...id10 === def.emailField || field2.type === "email" ? { emailIdentity: "literal" } : {}
|
|
1652
1715
|
};
|
|
1653
1716
|
});
|
|
1654
1717
|
}
|
|
@@ -1684,19 +1747,19 @@ function resolveLeaderFormation(input, crm) {
|
|
|
1684
1747
|
const required = input.required ?? (type === "chapter_application" ? DEFAULT_REQUIRED2 : []);
|
|
1685
1748
|
const optional = input.optional ?? (type === "chapter_application" ? DEFAULT_OPTIONAL2 : []);
|
|
1686
1749
|
for (const [name, values] of [["required", required], ["optional", optional]]) {
|
|
1687
|
-
if (!Array.isArray(values) || !values.every((
|
|
1750
|
+
if (!Array.isArray(values) || !values.every((field2) => typeof field2 === "string" && field2 !== "")) {
|
|
1688
1751
|
throw new Error(`defineChapter.formation.${name}: must be an array of field-name strings`);
|
|
1689
1752
|
}
|
|
1690
1753
|
}
|
|
1691
|
-
const duplicates = [...required, ...optional].filter((
|
|
1754
|
+
const duplicates = [...required, ...optional].filter((field2, index2, fields) => fields.indexOf(field2) !== index2);
|
|
1692
1755
|
if (duplicates.length) throw new Error(`defineChapter.formation: duplicate public field "${duplicates[0]}"`);
|
|
1693
|
-
const unknown = [...required, ...optional].filter((
|
|
1756
|
+
const unknown = [...required, ...optional].filter((field2) => !def.fields[field2]);
|
|
1694
1757
|
if (unknown.length) throw new Error(`defineChapter.formation: field "${unknown[0]}" is not declared on CRM type "${type}"`);
|
|
1695
|
-
const missingRequired = Object.entries(def.fields).filter(([,
|
|
1758
|
+
const missingRequired = Object.entries(def.fields).filter(([, field2]) => field2.required).map(([field2]) => field2).filter((field2) => !required.includes(field2));
|
|
1696
1759
|
if (missingRequired.length) {
|
|
1697
1760
|
throw new Error(`defineChapter.formation.required: must include required CRM field "${missingRequired[0]}"`);
|
|
1698
1761
|
}
|
|
1699
|
-
const unsupportedRequired = required.find((
|
|
1762
|
+
const unsupportedRequired = required.find((field2) => !["string", "email", "url", "enum"].includes(def.fields[field2].type));
|
|
1700
1763
|
if (unsupportedRequired) {
|
|
1701
1764
|
throw new Error(`defineChapter.formation.required: field "${unsupportedRequired}" must be string-like`);
|
|
1702
1765
|
}
|
|
@@ -1709,9 +1772,9 @@ function resolveLeaderFormation(input, crm) {
|
|
|
1709
1772
|
if (!Number.isSafeInteger(bodyCap) || bodyCap < 1024 || bodyCap > 131072) {
|
|
1710
1773
|
throw new Error("defineChapter.formation.bodyCap: must be an integer from 1024 to 131072");
|
|
1711
1774
|
}
|
|
1712
|
-
for (const [
|
|
1713
|
-
if (![...required, ...optional].includes(
|
|
1714
|
-
throw new Error(`defineChapter.formation.maxLen.${
|
|
1775
|
+
for (const [field2, cap] of Object.entries(input.maxLen ?? {})) {
|
|
1776
|
+
if (![...required, ...optional].includes(field2) || !Number.isSafeInteger(cap) || cap < 1 || cap > 32768) {
|
|
1777
|
+
throw new Error(`defineChapter.formation.maxLen.${field2}: must name a public field and be an integer from 1 to 32768`);
|
|
1715
1778
|
}
|
|
1716
1779
|
}
|
|
1717
1780
|
return {
|
|
@@ -1801,10 +1864,10 @@ function resolveNetwork(config, crm) {
|
|
|
1801
1864
|
}
|
|
1802
1865
|
const def = crm.config.types[type];
|
|
1803
1866
|
if (!def) throw new Error(`defineChapter.network.targets.${target.id}.fields: unknown leader CRM type "${type}"`);
|
|
1804
|
-
const unknown = names.filter((
|
|
1867
|
+
const unknown = names.filter((field2) => !def.fields[field2]);
|
|
1805
1868
|
if (unknown.length > 0) {
|
|
1806
1869
|
throw new Error(
|
|
1807
|
-
`defineChapter.network.targets.${target.id}.fields.${type}: ${unknown.map((
|
|
1870
|
+
`defineChapter.network.targets.${target.id}.fields.${type}: ${unknown.map((field2) => `"${field2}"`).join(", ")} not declared on the leader CRM type`
|
|
1808
1871
|
);
|
|
1809
1872
|
}
|
|
1810
1873
|
const nameField = def.nameField ?? "name";
|
|
@@ -1892,10 +1955,10 @@ function resolveNetwork(config, crm) {
|
|
|
1892
1955
|
if (!def) {
|
|
1893
1956
|
throw new Error(`defineChapter.network.readers.${reader.id}.fields: unknown local CRM type "${type}"`);
|
|
1894
1957
|
}
|
|
1895
|
-
const unknown = names.filter((
|
|
1958
|
+
const unknown = names.filter((field2) => !def.fields[field2]);
|
|
1896
1959
|
if (unknown.length > 0) {
|
|
1897
1960
|
throw new Error(
|
|
1898
|
-
`defineChapter.network.readers.${reader.id}.fields.${type}: ${unknown.map((
|
|
1961
|
+
`defineChapter.network.readers.${reader.id}.fields.${type}: ${unknown.map((field2) => `"${field2}"`).join(", ")} not declared on the local CRM type`
|
|
1899
1962
|
);
|
|
1900
1963
|
}
|
|
1901
1964
|
const nameField = def.nameField ?? "name";
|
|
@@ -2213,6 +2276,48 @@ function defineChapter(config) {
|
|
|
2213
2276
|
return chapter;
|
|
2214
2277
|
}
|
|
2215
2278
|
|
|
2279
|
+
// src/network-commercial-seats.ts
|
|
2280
|
+
function commercialSeatItems(object2) {
|
|
2281
|
+
const items = object2.items;
|
|
2282
|
+
if (!items || items.has_more !== false || !Array.isArray(items.data) || items.data.length > 100) return void 0;
|
|
2283
|
+
const result = [];
|
|
2284
|
+
for (const item of items.data) {
|
|
2285
|
+
const metadata = item.metadata;
|
|
2286
|
+
if (!metadata?.seatId || !metadata.membershipId) continue;
|
|
2287
|
+
const price = typeof item.price === "string" ? item.price : item.price?.id;
|
|
2288
|
+
if (typeof metadata.seatId !== "string" || !/^seat_[a-f0-9]{64}$/.test(metadata.seatId) || typeof metadata.membershipId !== "string" || !/^membership_[a-f0-9]{64}$/.test(metadata.membershipId) || typeof item.id !== "string" || !/^si_[A-Za-z0-9_]+$/.test(item.id) || typeof price !== "string" || !/^price_[A-Za-z0-9_]+$/.test(price) || item.quantity !== 1) continue;
|
|
2289
|
+
result.push({
|
|
2290
|
+
seatId: metadata.seatId,
|
|
2291
|
+
membershipId: metadata.membershipId,
|
|
2292
|
+
subscriptionItemId: item.id,
|
|
2293
|
+
priceId: price,
|
|
2294
|
+
...Number.isSafeInteger(item.current_period_end) ? { currentPeriodEnd: Number(item.current_period_end) } : {}
|
|
2295
|
+
});
|
|
2296
|
+
}
|
|
2297
|
+
return result;
|
|
2298
|
+
}
|
|
2299
|
+
function projectCommercialSeats(payments, subscriptions, refunds) {
|
|
2300
|
+
return payments.map((payment) => {
|
|
2301
|
+
const matches2 = subscriptions.flatMap((subscription) => subscription.sourceKey === payment.sourceKey && subscription.customerId === payment.customerId && subscription.applicationId === payment.applicationId ? (subscription.seatSubscriptionItems ?? []).filter((item) => item.seatId === payment.seatId && item.membershipId === payment.membershipId).map((item) => ({ subscription, item })) : []);
|
|
2302
|
+
const match = matches2.length === 1 ? matches2[0] : void 0;
|
|
2303
|
+
const refundedCents = refunds.filter((refund) => refund.sourceKey === payment.sourceKey && refund.currency === payment.currency && refund.paymentIntentId === payment.paymentIntentId && refund.status === "succeeded").reduce((sum, refund) => sum + Number(refund.amountCents ?? 0), 0);
|
|
2304
|
+
return {
|
|
2305
|
+
...payment,
|
|
2306
|
+
objectKind: "seat",
|
|
2307
|
+
objectId: payment.seatId,
|
|
2308
|
+
refundedCents,
|
|
2309
|
+
...refundedCents >= Number(payment.amountReceivedCents) && refundedCents > 0 ? { status: "refunded" } : {},
|
|
2310
|
+
...match ? {
|
|
2311
|
+
subscriptionId: match.subscription.subscriptionId,
|
|
2312
|
+
subscriptionItemId: match.item.subscriptionItemId,
|
|
2313
|
+
priceId: match.item.priceId,
|
|
2314
|
+
currentPeriodEnd: match.item.currentPeriodEnd ?? match.subscription.currentPeriodEnd,
|
|
2315
|
+
cancelAtPeriodEnd: match.subscription.cancelAtPeriodEnd
|
|
2316
|
+
} : {}
|
|
2317
|
+
};
|
|
2318
|
+
});
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2216
2321
|
// src/subscription-billing-terms.ts
|
|
2217
2322
|
import { stripeRequest } from "@odla-ai/stripe";
|
|
2218
2323
|
|
|
@@ -2384,6 +2489,7 @@ function normalizeNetworkCommercialStripeEvent(input, digest) {
|
|
|
2384
2489
|
...chargeId ? { chargeId } : {},
|
|
2385
2490
|
...paymentIntentId ? { paymentIntentId } : {},
|
|
2386
2491
|
...price,
|
|
2492
|
+
...objectKind === "subscription" && commercialSeatItems(object2) ? { seatSubscriptionItems: commercialSeatItems(object2) } : {},
|
|
2387
2493
|
...status2 ? { status: status2 } : {},
|
|
2388
2494
|
...currency ? { currency } : {},
|
|
2389
2495
|
...amountCents !== void 0 ? { amountCents } : {},
|
|
@@ -2405,8 +2511,8 @@ function normalizeNetworkCommercialStripeEvent(input, digest) {
|
|
|
2405
2511
|
var identityFields = ["appId", "chapterId", "environment", "dataEnvironment", "runtime"];
|
|
2406
2512
|
function directSource(fact, sources) {
|
|
2407
2513
|
const mode = fact.livemode ? "live" : "test";
|
|
2408
|
-
const supplied = identityFields.filter((
|
|
2409
|
-
const candidates = sources.filter((source) => source.stripeMode === mode && supplied.every((
|
|
2514
|
+
const supplied = identityFields.filter((field2) => fact.identity[field2] !== void 0);
|
|
2515
|
+
const candidates = sources.filter((source) => source.stripeMode === mode && supplied.every((field2) => source[field2] === fact.identity[field2]));
|
|
2410
2516
|
if (supplied.length === identityFields.length) return candidates.length === 1 ? candidates[0] : "wrong";
|
|
2411
2517
|
if (supplied.length > 0 && candidates.length === 0) return "wrong";
|
|
2412
2518
|
return null;
|
|
@@ -2502,11 +2608,11 @@ function projectNetworkCommercialEvents(input, sources) {
|
|
|
2502
2608
|
}
|
|
2503
2609
|
}
|
|
2504
2610
|
const totalsMap = /* @__PURE__ */ new Map();
|
|
2505
|
-
const add = (row2,
|
|
2611
|
+
const add = (row2, field2, amount) => {
|
|
2506
2612
|
if (!row2.currency) return;
|
|
2507
2613
|
const key3 = `${row2.sourceKey}:${row2.currency}`;
|
|
2508
2614
|
const total = totalsMap.get(key3) ?? { sourceKey: row2.sourceKey, currency: row2.currency, paidCents: 0, refundedCents: 0, netCents: 0 };
|
|
2509
|
-
total[
|
|
2615
|
+
total[field2] += amount;
|
|
2510
2616
|
total.netCents = total.paidCents - total.refundedCents;
|
|
2511
2617
|
totalsMap.set(key3, total);
|
|
2512
2618
|
};
|
|
@@ -2514,7 +2620,7 @@ function projectNetworkCommercialEvents(input, sources) {
|
|
|
2514
2620
|
for (const row2 of seatPayments) add(row2, "paidCents", row2.amountReceivedCents ?? 0);
|
|
2515
2621
|
for (const row2 of refunds) add(row2, "refundedCents", row2.amountCents ?? 0);
|
|
2516
2622
|
return {
|
|
2517
|
-
...seatPayments.length ? { seatPayments } : {},
|
|
2623
|
+
...seatPayments.length ? { seatPayments, seats: projectCommercialSeats(seatPayments, subscriptions, refunds) } : {},
|
|
2518
2624
|
customers,
|
|
2519
2625
|
subscriptions,
|
|
2520
2626
|
invoices,
|
|
@@ -3088,8 +3194,8 @@ function createChapterIntegration(chapter, options = {}) {
|
|
|
3088
3194
|
}
|
|
3089
3195
|
|
|
3090
3196
|
// src/membership-policy.ts
|
|
3091
|
-
var fail2 = (
|
|
3092
|
-
throw new TypeError(`invalid membership policy: ${
|
|
3197
|
+
var fail2 = (field2) => {
|
|
3198
|
+
throw new TypeError(`invalid membership policy: ${field2}`);
|
|
3093
3199
|
};
|
|
3094
3200
|
var object = (value) => {
|
|
3095
3201
|
if (!value || typeof value !== "object" || Array.isArray(value)) return fail2("object");
|
|
@@ -3286,7 +3392,8 @@ async function issueMembershipCommercialOffer(db, input) {
|
|
|
3286
3392
|
const offerings = rows3.membershipOfferingVersion.map((row3) => membershipOfferingVersion(row3.snapshot)).filter((offer) => offer.chapterId === input.chapterId && offer.baseSkuId === input.skuId && offer.status === "published" && input.now >= offer.effectiveFrom && (offer.effectiveUntil === void 0 || input.now < offer.effectiveUntil));
|
|
3287
3393
|
const policies = rows3.discountPolicyVersion.map((row3) => membershipDiscountPolicyVersion(row3.snapshot)).filter((policy2) => policy2.chapterId === input.chapterId && policy2.status === "published" && policy2.cohortEligibleSkuIds.includes(input.skuId) && input.now >= policy2.qualifyingStartsAt && input.now < policy2.qualifyingEndsAt);
|
|
3288
3394
|
const offering = offerings.sort((a, b) => b.version - a.version)[0];
|
|
3289
|
-
const
|
|
3395
|
+
const restart = await load(db, "membershipRestart", (await membershipOfferIdForRequest({ ...input, requestId: "checkout_binding" })).replace(/^offer_/, "membership_"));
|
|
3396
|
+
const policy = restart ? void 0 : policies.sort((a, b) => b.version - a.version)[0];
|
|
3290
3397
|
if (!offering || offerings.filter((item) => item.version === offering.version).length !== 1 || policy && policies.filter((item) => item.version === policy.version).length !== 1) reject("ambiguous_membership_policy");
|
|
3291
3398
|
const approvedFoundingCount = policy ? state.cohortCounts[policy.cohortLabel] ?? 0 : 0;
|
|
3292
3399
|
if (!Number.isSafeInteger(approvedFoundingCount) || Number(approvedFoundingCount) < 0) reject("invalid_cohort_count");
|
|
@@ -3324,8 +3431,73 @@ async function issueMembershipCommercialOffer(db, input) {
|
|
|
3324
3431
|
return restore(stored, input);
|
|
3325
3432
|
}
|
|
3326
3433
|
|
|
3327
|
-
// src/membership-authority
|
|
3328
|
-
import {
|
|
3434
|
+
// src/membership-cancel-authority.ts
|
|
3435
|
+
import { stripeRequest as stripeRequest2 } from "@odla-ai/stripe";
|
|
3436
|
+
|
|
3437
|
+
// src/commercial-evidence-guard.ts
|
|
3438
|
+
async function commercialEvidenceGuard(db, eventId, digest, code = "membership_payment_evidence_quarantined") {
|
|
3439
|
+
const ns = "networkCommercialExceptions", id10 = `network-commercial-exception:${eventId}:collision`;
|
|
3440
|
+
const row2 = (await db.query({ [ns]: { $: { where: { id: id10 }, limit: 1 } } }))[ns]?.[0];
|
|
3441
|
+
if (!row2) return { ns, id: id10, exists: false };
|
|
3442
|
+
if (row2.status !== "resolved" || row2.resolvedDigest !== digest || typeof digest !== "string") {
|
|
3443
|
+
throw Object.assign(new Error(code), { code });
|
|
3444
|
+
}
|
|
3445
|
+
return { ns, id: id10, exists: true, equals: { status: "resolved", resolvedDigest: digest, updatedAt: row2.updatedAt } };
|
|
3446
|
+
}
|
|
3447
|
+
|
|
3448
|
+
// src/membership-authority-state.ts
|
|
3449
|
+
var membershipFail = (code) => {
|
|
3450
|
+
throw Object.assign(new Error(code), { code });
|
|
3451
|
+
};
|
|
3452
|
+
var membershipLoad = async (db, ns, id10) => (await db.query({ [ns]: { $: { where: { id: id10 }, limit: 1 } } }))[ns]?.[0];
|
|
3453
|
+
var membershipScopeKeys = ["appId", "chapterId", "environment", "runtime", "applicationId", "skuId"];
|
|
3454
|
+
var membershipScope = (input) => Object.fromEntries(membershipScopeKeys.map((key3) => [key3, input[key3]]));
|
|
3455
|
+
function requireMembershipScope(row2, input) {
|
|
3456
|
+
if (!row2 || membershipScopeKeys.some((key3) => row2[key3] !== input[key3])) membershipFail("membership_scope_mismatch");
|
|
3457
|
+
}
|
|
3458
|
+
async function loadMembershipAuthorityState(db, input) {
|
|
3459
|
+
const row2 = await membershipLoad(db, "membershipAuthorityState", `${input.chapterId}:${input.environment}`);
|
|
3460
|
+
if (!row2 || row2.mode !== "authoritative" || row2.chapterId !== input.chapterId || row2.environment !== input.environment || !Number.isSafeInteger(row2.revision) || Number(row2.revision) < 1) return membershipFail("membership_authority_unavailable");
|
|
3461
|
+
return row2;
|
|
3462
|
+
}
|
|
3463
|
+
var membershipUpdate = (ns, id10, attrs) => ({ t: "update", ns, id: id10, attrs });
|
|
3464
|
+
var membershipClock = (now) => {
|
|
3465
|
+
if (!Number.isSafeInteger(now) || now < 0) membershipFail("invalid_membership_clock");
|
|
3466
|
+
};
|
|
3467
|
+
|
|
3468
|
+
// src/membership-restart-payment.ts
|
|
3469
|
+
async function membershipRestartPayment(db, target, member, now) {
|
|
3470
|
+
const id10 = String(member.id), restart = await membershipLoad(db, "membershipRestart", id10);
|
|
3471
|
+
requireMembershipScope(restart, target);
|
|
3472
|
+
const entitlement = await membershipLoad(db, "networkEntitlement", `${id10}:primary`);
|
|
3473
|
+
requireMembershipScope(entitlement, target);
|
|
3474
|
+
if (entitlement.membershipId !== id10 || entitlement.kind !== "primary") membershipFail("membership_restart_conflict");
|
|
3475
|
+
if (restart.status !== "ready" || member.status !== "restarting" || member.restartId !== restart.restartId || member.memberNumber !== restart.memberNumber || !Number.isSafeInteger(member.journeyApprovedAt)) membershipFail("membership_restart_conflict");
|
|
3476
|
+
return {
|
|
3477
|
+
restartId: String(restart.restartId),
|
|
3478
|
+
entitlementAttrs: { revision: Number(entitlement.revision) + 1, createdAt: entitlement.createdAt },
|
|
3479
|
+
attrs: {
|
|
3480
|
+
memberNumber: member.memberNumber,
|
|
3481
|
+
journeyApprovedAt: member.journeyApprovedAt,
|
|
3482
|
+
founding: member.founding,
|
|
3483
|
+
cohort: member.cohort,
|
|
3484
|
+
...member.personId ? { personId: member.personId } : {},
|
|
3485
|
+
status: "active",
|
|
3486
|
+
revision: Number(member.revision) + 1,
|
|
3487
|
+
createdAt: member.createdAt,
|
|
3488
|
+
graceEndsAt: 0
|
|
3489
|
+
},
|
|
3490
|
+
ops: [
|
|
3491
|
+
membershipUpdate("membershipRestart", id10, { status: "complete" }),
|
|
3492
|
+
{ t: "retract", ns: "networkMembership", id: id10, attrs: ["refundRequestedAt", "cancellationRequestedAt", "discountPolicyVersionId", "discountSnapshot"] }
|
|
3493
|
+
],
|
|
3494
|
+
guards: [
|
|
3495
|
+
{ ns: "networkMembership", id: id10, exists: true, equals: { status: "restarting", revision: member.revision, restartId: restart.restartId } },
|
|
3496
|
+
{ ns: "membershipRestart", id: id10, exists: true, equals: { status: "ready", restartId: restart.restartId } },
|
|
3497
|
+
{ ns: "networkEntitlement", id: String(entitlement.id), exists: true, equals: { revision: entitlement.revision, status: entitlement.status } }
|
|
3498
|
+
]
|
|
3499
|
+
};
|
|
3500
|
+
}
|
|
3329
3501
|
|
|
3330
3502
|
// src/membership-checkout-binding.ts
|
|
3331
3503
|
var keys = ["appId", "chapterId", "environment", "runtime", "applicationId", "skuId", "offerId", "offerDigest", "quoteDigest", "priceId"];
|
|
@@ -3352,10 +3524,11 @@ async function bindMembershipCheckout(db, input, now) {
|
|
|
3352
3524
|
const row2 = await load2(db, "membershipCheckoutOffer", input.offerId);
|
|
3353
3525
|
const offer = await verifyIssuedMembershipOffer(row2, { ...input, now, lifetimeMs: 6e4 });
|
|
3354
3526
|
if (offer.digest !== input.offerDigest || offer.expiresAt <= now || offer.issuedAt > now) return reject2("membership_offer_expired");
|
|
3527
|
+
const restart = await load2(db, "membershipRestart", id10.replace(/^checkout_/, "membership_"));
|
|
3355
3528
|
const receipt = { id: id10, ...Object.fromEntries(keys.map((key3) => [key3, input[key3]])), boundAt: now };
|
|
3356
3529
|
try {
|
|
3357
3530
|
await db.transact([{ t: "update", ns: "membershipOfferCheckout", id: id10, attrs: receipt }], {
|
|
3358
|
-
mutationId: `membership-checkout:${id10}`,
|
|
3531
|
+
mutationId: `membership-checkout:${id10}${restart?.restartId ? `:${restart.restartId}` : ""}`,
|
|
3359
3532
|
guards: [
|
|
3360
3533
|
{ ns: "membershipOfferCheckout", id: id10, exists: false },
|
|
3361
3534
|
{ ns: "membershipCheckoutOffer", id: input.offerId, exists: true, equals: { digest: input.offerDigest } },
|
|
@@ -3370,26 +3543,6 @@ async function bindMembershipCheckout(db, input, now) {
|
|
|
3370
3543
|
return verifyMembershipCheckoutBinding(saved, input);
|
|
3371
3544
|
}
|
|
3372
3545
|
|
|
3373
|
-
// src/membership-authority-state.ts
|
|
3374
|
-
var membershipFail = (code) => {
|
|
3375
|
-
throw Object.assign(new Error(code), { code });
|
|
3376
|
-
};
|
|
3377
|
-
var membershipLoad = async (db, ns, id10) => (await db.query({ [ns]: { $: { where: { id: id10 }, limit: 1 } } }))[ns]?.[0];
|
|
3378
|
-
var membershipScopeKeys = ["appId", "chapterId", "environment", "runtime", "applicationId", "skuId"];
|
|
3379
|
-
var membershipScope = (input) => Object.fromEntries(membershipScopeKeys.map((key3) => [key3, input[key3]]));
|
|
3380
|
-
function requireMembershipScope(row2, input) {
|
|
3381
|
-
if (!row2 || membershipScopeKeys.some((key3) => row2[key3] !== input[key3])) membershipFail("membership_scope_mismatch");
|
|
3382
|
-
}
|
|
3383
|
-
async function loadMembershipAuthorityState(db, input) {
|
|
3384
|
-
const row2 = await membershipLoad(db, "membershipAuthorityState", `${input.chapterId}:${input.environment}`);
|
|
3385
|
-
if (!row2 || row2.mode !== "authoritative" || row2.chapterId !== input.chapterId || row2.environment !== input.environment || !Number.isSafeInteger(row2.revision) || Number(row2.revision) < 1) return membershipFail("membership_authority_unavailable");
|
|
3386
|
-
return row2;
|
|
3387
|
-
}
|
|
3388
|
-
var membershipUpdate = (ns, id10, attrs) => ({ t: "update", ns, id: id10, attrs });
|
|
3389
|
-
var membershipClock = (now) => {
|
|
3390
|
-
if (!Number.isSafeInteger(now) || now < 0) membershipFail("invalid_membership_clock");
|
|
3391
|
-
};
|
|
3392
|
-
|
|
3393
3546
|
// src/membership-payment-authority.ts
|
|
3394
3547
|
async function networkMembershipId(input) {
|
|
3395
3548
|
return (await membershipCheckoutBindingId(input)).replace(/^checkout_/, "membership_");
|
|
@@ -3407,9 +3560,7 @@ async function confirmMembershipPayment(db, input, now) {
|
|
|
3407
3560
|
if (binding.offerDigest !== offer.digest) membershipFail("membership_checkout_conflict");
|
|
3408
3561
|
const eventRow = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
|
|
3409
3562
|
if (!eventRow) membershipFail("membership_payment_evidence_pending");
|
|
3410
|
-
|
|
3411
|
-
membershipFail("membership_payment_evidence_quarantined");
|
|
3412
|
-
}
|
|
3563
|
+
const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, eventRow.digest);
|
|
3413
3564
|
const fact = eventRow.fact;
|
|
3414
3565
|
if (!fact || !fact.identity || !/^[a-f0-9]{64}$/.test(String(eventRow.digest)) || fact.digest !== eventRow.digest || fact.eventId !== input.eventId || fact.eventType !== "invoice.payment_succeeded" || fact.objectKind !== "invoice" || fact.billingReason !== "subscription_create" || fact.livemode !== (input.environment === "prod") || fact.status !== "paid" || fact.applicationId !== input.applicationId || fact.subscriptionId !== input.providerSubscriptionId || fact.customerId !== input.providerCustomerId || !/^in_[A-Za-z0-9_]+$/.test(fact.invoiceId ?? "") || fact.currency !== offer.contract.offering.currency || fact.invoiceTotalCents !== offer.contract.dueTodayCents || fact.amountPaidCents !== offer.contract.dueTodayCents || Object.entries({
|
|
3415
3566
|
appId: input.appId,
|
|
@@ -3425,8 +3576,9 @@ async function confirmMembershipPayment(db, input, now) {
|
|
|
3425
3576
|
if (!Number.isSafeInteger(paidAt) || Number(paidAt) < Math.floor(Number(binding.boundAt) / 1e3) * 1e3 || Number(paidAt) > now + 3e4 || !Number.isSafeInteger(endsAt) || Number(endsAt) <= Number(paidAt)) membershipFail("membership_payment_period_mismatch");
|
|
3426
3577
|
const id10 = await networkMembershipId(input), entitlementId = `${id10}:primary`;
|
|
3427
3578
|
let member = await membershipLoad(db, "networkMembership", id10);
|
|
3428
|
-
if (!member) {
|
|
3579
|
+
if (!member || member.status === "restarting") {
|
|
3429
3580
|
const offering = offer.contract.offering;
|
|
3581
|
+
const restart = member ? await membershipRestartPayment(db, input, member, now) : null;
|
|
3430
3582
|
const attrs = {
|
|
3431
3583
|
id: id10,
|
|
3432
3584
|
membershipId: id10,
|
|
@@ -3458,7 +3610,8 @@ async function confirmMembershipPayment(db, input, now) {
|
|
|
3458
3610
|
refundedCents: 0,
|
|
3459
3611
|
revision: 1,
|
|
3460
3612
|
createdAt: now,
|
|
3461
|
-
updatedAt: now
|
|
3613
|
+
updatedAt: now,
|
|
3614
|
+
...restart?.attrs
|
|
3462
3615
|
};
|
|
3463
3616
|
try {
|
|
3464
3617
|
await db.transact([
|
|
@@ -3468,21 +3621,23 @@ async function confirmMembershipPayment(db, input, now) {
|
|
|
3468
3621
|
...membershipScope(input),
|
|
3469
3622
|
membershipId: id10,
|
|
3470
3623
|
kind: "primary",
|
|
3471
|
-
status: "pending_approval",
|
|
3624
|
+
status: restart?.attrs.status === "active" ? "active" : "pending_approval",
|
|
3472
3625
|
termStartedAt: paidAt,
|
|
3473
3626
|
termEndsAt: endsAt,
|
|
3474
3627
|
offeringSnapshot: offering,
|
|
3475
3628
|
sourceEventId: input.eventId,
|
|
3476
3629
|
revision: 1,
|
|
3477
3630
|
createdAt: now,
|
|
3478
|
-
updatedAt: now
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3631
|
+
updatedAt: now,
|
|
3632
|
+
...restart?.entitlementAttrs
|
|
3633
|
+
}),
|
|
3634
|
+
...restart?.ops ?? []
|
|
3635
|
+
], { mutationId: `membership-paid:${id10}${restart ? `:${restart.restartId}` : ""}`, guards: [
|
|
3636
|
+
...restart?.guards ?? [{ ns: "networkMembership", id: id10, exists: false }],
|
|
3482
3637
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
|
|
3483
3638
|
{ ns: "membershipOfferCheckout", id: String(binding.id), exists: true, equals: { offerDigest: offer.digest } },
|
|
3484
3639
|
{ ns: "networkCommercialEvents", id: String(eventRow.id), exists: true, equals: { digest: eventRow.digest } },
|
|
3485
|
-
|
|
3640
|
+
evidenceGuard
|
|
3486
3641
|
] });
|
|
3487
3642
|
} catch (error) {
|
|
3488
3643
|
if (error.code !== "transact_guard_failed") throw error;
|
|
@@ -3501,6 +3656,57 @@ async function confirmMembershipPayment(db, input, now) {
|
|
|
3501
3656
|
};
|
|
3502
3657
|
}
|
|
3503
3658
|
|
|
3659
|
+
// src/membership-cancel-authority.ts
|
|
3660
|
+
async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
|
|
3661
|
+
membershipClock(now);
|
|
3662
|
+
const id10 = await networkMembershipId(target);
|
|
3663
|
+
const state = await loadMembershipAuthorityState(db, target);
|
|
3664
|
+
const member = await membershipLoad(db, "networkMembership", id10);
|
|
3665
|
+
requireMembershipScope(member, target);
|
|
3666
|
+
if (!["active", "cancel_at_term_end", "payment_grace", "paid_pending_approval", "lapsed"].includes(String(member.status))) {
|
|
3667
|
+
membershipFail("membership_cancellation_unavailable");
|
|
3668
|
+
}
|
|
3669
|
+
const path = `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`;
|
|
3670
|
+
const read2 = await stripeRequest2({ secretKey: stripeSecret }, "GET", path);
|
|
3671
|
+
const sub = read2.body;
|
|
3672
|
+
if (!read2.ok || sub.id !== member.providerSubscriptionId || sub.customer !== member.providerCustomerId || sub.livemode !== (target.environment === "prod") || !Object.entries({
|
|
3673
|
+
...stripePartitionMetadata(target),
|
|
3674
|
+
applicationId: target.applicationId
|
|
3675
|
+
}).every(([key3, value]) => sub.metadata?.[key3] === value)) membershipFail("membership_cancellation_provider_mismatch");
|
|
3676
|
+
if (sub.status !== "canceled" && sub.cancel_at_period_end !== true) {
|
|
3677
|
+
const write = await stripeRequest2(
|
|
3678
|
+
{ secretKey: stripeSecret },
|
|
3679
|
+
"POST",
|
|
3680
|
+
path,
|
|
3681
|
+
{ cancel_at_period_end: true, proration_behavior: "none" },
|
|
3682
|
+
{ idempotencyKey: `membership-cancel-at-term:${id10}:${member.providerSubscriptionId}` }
|
|
3683
|
+
);
|
|
3684
|
+
if (!write.ok || write.body.id !== member.providerSubscriptionId || write.body.cancel_at_period_end !== true) {
|
|
3685
|
+
membershipFail("membership_cancellation_pending");
|
|
3686
|
+
}
|
|
3687
|
+
}
|
|
3688
|
+
const status2 = Number(member.termEndsAt) <= now ? "lapsed" : member.journeyApprovedAt ? "cancel_at_term_end" : "paid_pending_approval";
|
|
3689
|
+
if (member.status !== status2 || member.autoRenew !== false || Number(member.graceEndsAt ?? 0) !== 0) {
|
|
3690
|
+
await db.transact([membershipUpdate("networkMembership", id10, {
|
|
3691
|
+
status: status2,
|
|
3692
|
+
autoRenew: false,
|
|
3693
|
+
graceEndsAt: 0,
|
|
3694
|
+
cancellationRequestedAt: now,
|
|
3695
|
+
revision: Number(member.revision) + 1,
|
|
3696
|
+
updatedAt: now
|
|
3697
|
+
})], {
|
|
3698
|
+
guards: [
|
|
3699
|
+
{ ns: "networkMembership", id: id10, exists: true, equals: { revision: member.revision } },
|
|
3700
|
+
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { revision: state.revision, mode: "authoritative" } }
|
|
3701
|
+
]
|
|
3702
|
+
});
|
|
3703
|
+
}
|
|
3704
|
+
return { membershipId: id10, status: status2, termEndsAt: Number(member.termEndsAt), graceEndsAt: 0, autoRenew: false };
|
|
3705
|
+
}
|
|
3706
|
+
|
|
3707
|
+
// src/membership-authority-handler.ts
|
|
3708
|
+
import { FEDERATION_HEADERS, verifyFederatedRequest } from "@odla-ai/db";
|
|
3709
|
+
|
|
3504
3710
|
// src/membership-seat-activation.ts
|
|
3505
3711
|
async function namedSeatPrimaryActivation(db, member, now) {
|
|
3506
3712
|
if (!member.namedSeatId) return { ops: [], guards: [] };
|
|
@@ -3593,7 +3799,7 @@ async function approveNetworkMembership(db, input, now) {
|
|
|
3593
3799
|
}
|
|
3594
3800
|
|
|
3595
3801
|
// src/payments-refund.ts
|
|
3596
|
-
import { stripeRequest as
|
|
3802
|
+
import { stripeRequest as stripeRequest3 } from "@odla-ai/stripe";
|
|
3597
3803
|
var row = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
3598
3804
|
var id7 = (value) => typeof value === "string" ? value : String(row(value).id ?? "");
|
|
3599
3805
|
var providerId2 = (value, prefix) => new RegExp(`^${prefix}_[A-Za-z0-9]+$`).test(value);
|
|
@@ -3603,7 +3809,7 @@ function fail3(code = "refund_target_mismatch") {
|
|
|
3603
3809
|
async function refundSubscriptionPayment(secretKey, input) {
|
|
3604
3810
|
if (!providerId2(input.customerId, "cus") || !providerId2(input.subscriptionId, "sub")) fail3();
|
|
3605
3811
|
const call = async (method, path, params, key3) => {
|
|
3606
|
-
const result = await
|
|
3812
|
+
const result = await stripeRequest3({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
|
|
3607
3813
|
if (!result.ok) fail3("stripe_error");
|
|
3608
3814
|
if (!result.body || typeof result.body !== "object" || Array.isArray(result.body)) fail3("stripe_invalid_response");
|
|
3609
3815
|
return result.body;
|
|
@@ -3774,12 +3980,13 @@ async function refundNetworkMembership(db, input, secretKey, now) {
|
|
|
3774
3980
|
}
|
|
3775
3981
|
|
|
3776
3982
|
// src/membership-provider-lifecycle.ts
|
|
3777
|
-
import { stripeRequest as
|
|
3983
|
+
import { stripeRequest as stripeRequest4 } from "@odla-ai/stripe";
|
|
3778
3984
|
|
|
3779
3985
|
// src/membership-renewal-authority.ts
|
|
3780
3986
|
async function reconcileMembershipRenewal(db, input, now) {
|
|
3781
3987
|
membershipClock(now);
|
|
3782
3988
|
const id10 = await networkMembershipId(input), eventRow = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
|
|
3989
|
+
const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, eventRow?.digest, "membership_renewal_evidence_pending");
|
|
3783
3990
|
const fact = eventRow?.fact;
|
|
3784
3991
|
if (!fact || !/^evt_[A-Za-z0-9_]+$/.test(input.eventId) || !/^[a-f0-9]{64}$/.test(String(eventRow.digest)) || fact.digest !== eventRow.digest || fact.eventId !== input.eventId || fact.objectKind !== "invoice" || !["invoice.payment_succeeded", "invoice.payment_failed"].includes(fact.eventType) || fact.billingReason !== "subscription_cycle" || fact.livemode !== (input.environment === "prod") || fact.applicationId !== input.applicationId || Object.entries({
|
|
3785
3992
|
appId: input.appId,
|
|
@@ -3787,7 +3994,7 @@ async function reconcileMembershipRenewal(db, input, now) {
|
|
|
3787
3994
|
environment: input.environment,
|
|
3788
3995
|
dataEnvironment: input.environment,
|
|
3789
3996
|
runtime: input.runtime
|
|
3790
|
-
}).some(([key3, value]) => fact.identity[key3] !== value)
|
|
3997
|
+
}).some(([key3, value]) => fact.identity[key3] !== value)) {
|
|
3791
3998
|
return membershipFail("membership_renewal_evidence_pending");
|
|
3792
3999
|
}
|
|
3793
4000
|
for (let attempt = 0; attempt < 8; attempt++) {
|
|
@@ -3801,7 +4008,7 @@ async function reconcileMembershipRenewal(db, input, now) {
|
|
|
3801
4008
|
autoRenew: member.autoRenew === true
|
|
3802
4009
|
});
|
|
3803
4010
|
if (fact.subscriptionId !== member.providerSubscriptionId || fact.customerId !== member.providerCustomerId || fact.currency !== member.currency) membershipFail("membership_renewal_scope_mismatch");
|
|
3804
|
-
if (["refunded", "refund_pending", "canceled", "declined"].includes(String(member.status))) return receipt();
|
|
4011
|
+
if (["refunded", "refund_pending", "canceled", "declined", "restarting", "superseded"].includes(String(member.status))) return receipt();
|
|
3805
4012
|
const lines = fact.recurringInvoiceLines;
|
|
3806
4013
|
if (!lines?.length || lines.length > 2) membershipFail("membership_renewal_lines_missing");
|
|
3807
4014
|
const start = lines[0].startsAt, end = lines[0].endsAt;
|
|
@@ -3810,7 +4017,7 @@ async function reconcileMembershipRenewal(db, input, now) {
|
|
|
3810
4017
|
if (start !== member.termEndsAt || start > now + 3e4) membershipFail("membership_renewal_period_gap");
|
|
3811
4018
|
const seat = member.namedSeatId ? await membershipLoad(db, "networkSeat", String(member.namedSeatId)) : void 0;
|
|
3812
4019
|
if (seat) requireMembershipScope(seat, input);
|
|
3813
|
-
const billedSeat = seat && seat.providerSubscriptionItemId && !["refunded", "refund_pending"].includes(String(seat.status)) ? seat : void 0;
|
|
4020
|
+
const billedSeat = seat && seat.providerSubscriptionId === member.providerSubscriptionId && seat.providerSubscriptionItemId && !["refunded", "refund_pending"].includes(String(seat.status)) ? seat : void 0;
|
|
3814
4021
|
const prices = [String(member.providerPriceId), ...billedSeat ? [String(billedSeat.priceId)] : []].sort();
|
|
3815
4022
|
const amount = Number(member.renewalAmountCents) + Number(billedSeat?.amountCents ?? 0);
|
|
3816
4023
|
if (!Number.isSafeInteger(amount) || amount <= 0 || fact.invoiceTotalCents !== amount || JSON.stringify(lines.map((line) => line.priceId).sort()) !== JSON.stringify(prices)) membershipFail("membership_renewal_amount_mismatch");
|
|
@@ -3843,7 +4050,7 @@ async function reconcileMembershipRenewal(db, input, now) {
|
|
|
3843
4050
|
{ ns: "networkEntitlement", id: `${id10}:primary`, exists: true, equals: { revision: entitlement.revision } },
|
|
3844
4051
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { revision: state.revision, mode: "authoritative" } },
|
|
3845
4052
|
{ ns: "networkCommercialEvents", id: String(eventRow.id), exists: true, equals: { digest: eventRow.digest } },
|
|
3846
|
-
|
|
4053
|
+
evidenceGuard
|
|
3847
4054
|
];
|
|
3848
4055
|
if (seat) guards.push({ ns: "networkSeat", id: String(seat.id), exists: true, equals: { revision: seat.revision } });
|
|
3849
4056
|
if (billedSeat?.entitlementId) {
|
|
@@ -3889,14 +4096,14 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
3889
4096
|
const row2 = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
|
|
3890
4097
|
const fact = row2?.fact;
|
|
3891
4098
|
if (fact?.objectKind === "invoice") return reconcileMembershipRenewal(db, input, now);
|
|
3892
|
-
const
|
|
4099
|
+
const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, row2?.digest, "membership_billing_evidence_pending");
|
|
3893
4100
|
if (!fact || fact.eventId !== input.eventId || fact.digest !== row2.digest || !/^[a-f0-9]{64}$/.test(fact.digest) || !["customer.subscription.updated", "customer.subscription.deleted", "charge.refunded"].includes(fact.eventType) || fact.livemode !== (input.environment === "prod") || fact.applicationId !== input.applicationId || !fact.identity || Object.entries({
|
|
3894
4101
|
appId: input.appId,
|
|
3895
4102
|
chapterId: input.chapterId,
|
|
3896
4103
|
environment: input.environment,
|
|
3897
4104
|
dataEnvironment: input.environment,
|
|
3898
4105
|
runtime: input.runtime
|
|
3899
|
-
}).some(([key3, value]) => fact.identity[key3] !== value)
|
|
4106
|
+
}).some(([key3, value]) => fact.identity[key3] !== value)) membershipFail("membership_billing_evidence_pending");
|
|
3900
4107
|
const sk = await getVaultSecret(db, stripeSecretName);
|
|
3901
4108
|
if (!sk) membershipFail("membership_billing_provider_unavailable");
|
|
3902
4109
|
const id10 = await networkMembershipId(input);
|
|
@@ -3910,6 +4117,7 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
3910
4117
|
graceEndsAt: Number(value.graceEndsAt ?? 0),
|
|
3911
4118
|
autoRenew: value.autoRenew === true
|
|
3912
4119
|
});
|
|
4120
|
+
if (["restarting"].includes(String(member.status))) return receipt();
|
|
3913
4121
|
if (fact.customerId !== member.providerCustomerId) membershipFail("membership_billing_scope_mismatch");
|
|
3914
4122
|
if (fact.eventType === "charge.refunded") {
|
|
3915
4123
|
if (!fact.chargeId || fact.seatId || fact.amountCents !== member.amountCents || fact.currency !== member.currency) membershipFail("membership_refund_evidence_mismatch");
|
|
@@ -3922,15 +4130,15 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
3922
4130
|
alreadyRefundedChargeId: fact.chargeId,
|
|
3923
4131
|
cancelSubscription: false
|
|
3924
4132
|
});
|
|
3925
|
-
|
|
4133
|
+
await commercialEvidenceGuard(db, input.eventId, row2?.digest, "membership_billing_evidence_pending");
|
|
3926
4134
|
await refundNetworkMembership(db, input, sk, now);
|
|
3927
4135
|
const saved2 = await membershipLoad(db, "networkMembership", id10);
|
|
3928
4136
|
requireMembershipScope(saved2, input);
|
|
3929
4137
|
return receipt(saved2);
|
|
3930
4138
|
}
|
|
3931
4139
|
if (fact.subscriptionId !== member.providerSubscriptionId || fact.objectKind !== "subscription") membershipFail("membership_billing_scope_mismatch");
|
|
3932
|
-
if (["refund_pending", "refunded", "declined", "canceled"].includes(String(member.status))) return receipt();
|
|
3933
|
-
const result = await
|
|
4140
|
+
if (["refund_pending", "refunded", "declined", "canceled", "restarting"].includes(String(member.status))) return receipt();
|
|
4141
|
+
const result = await stripeRequest4({ secretKey: sk }, "GET", `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`);
|
|
3934
4142
|
const sub = result.body;
|
|
3935
4143
|
if (!result.ok || sub.id !== member.providerSubscriptionId || sub.customer !== member.providerCustomerId || sub.livemode !== (input.environment === "prod") || typeof sub.cancel_at_period_end !== "boolean" || !["active", "trialing", "past_due", "unpaid", "canceled"].includes(sub.status) || !Object.entries({ ...stripePartitionMetadata(input), applicationId: input.applicationId }).every(([key3, value]) => sub.metadata?.[key3] === value)) {
|
|
3936
4144
|
membershipFail("membership_billing_provider_mismatch");
|
|
@@ -3954,7 +4162,7 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
3954
4162
|
{ ns: "networkEntitlement", id: String(entitlement.id), exists: true, equals: { revision: entitlement.revision } },
|
|
3955
4163
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
|
|
3956
4164
|
{ ns: "networkCommercialEvents", id: String(row2.id), exists: true, equals: { digest: fact.digest } },
|
|
3957
|
-
|
|
4165
|
+
evidenceGuard
|
|
3958
4166
|
] });
|
|
3959
4167
|
} catch (error) {
|
|
3960
4168
|
if (error.code !== "transact_guard_failed") throw error;
|
|
@@ -3967,6 +4175,120 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
3967
4175
|
return membershipFail("membership_billing_retry_required");
|
|
3968
4176
|
}
|
|
3969
4177
|
|
|
4178
|
+
// src/membership-restart-seat.ts
|
|
4179
|
+
async function expireRestartedMembershipSeat(db, target, member, now) {
|
|
4180
|
+
const ops = [], guards = [];
|
|
4181
|
+
if (!member.namedSeatId) return { ops, guards };
|
|
4182
|
+
const seat = await membershipLoad(db, "networkSeat", String(member.namedSeatId));
|
|
4183
|
+
requireMembershipScope(seat, target);
|
|
4184
|
+
if (!["refunded", "refund_pending", "expired"].includes(String(seat.status))) {
|
|
4185
|
+
ops.push(membershipUpdate("networkSeat", String(seat.id), { status: "expired", revision: Number(seat.revision) + 1, updatedAt: now }));
|
|
4186
|
+
guards.push({ ns: "networkSeat", id: String(seat.id), exists: true, equals: { revision: seat.revision } });
|
|
4187
|
+
}
|
|
4188
|
+
if (seat.entitlementId) {
|
|
4189
|
+
const entitlement = await membershipLoad(db, "networkEntitlement", String(seat.entitlementId));
|
|
4190
|
+
requireMembershipScope(entitlement, target);
|
|
4191
|
+
ops.push(membershipUpdate("networkEntitlement", String(entitlement.id), { status: "closed", revision: Number(entitlement.revision) + 1, updatedAt: now }));
|
|
4192
|
+
guards.push({ ns: "networkEntitlement", id: String(entitlement.id), exists: true, equals: { revision: entitlement.revision } });
|
|
4193
|
+
}
|
|
4194
|
+
return { ops, guards };
|
|
4195
|
+
}
|
|
4196
|
+
|
|
4197
|
+
// src/membership-restart-authority.ts
|
|
4198
|
+
import { stripeRequest as stripeRequest5 } from "@odla-ai/stripe";
|
|
4199
|
+
|
|
4200
|
+
// src/stripe-event-digest.ts
|
|
4201
|
+
async function stripePayloadDigest(value) {
|
|
4202
|
+
const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
|
|
4203
|
+
return [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
4204
|
+
}
|
|
4205
|
+
|
|
4206
|
+
// src/membership-restart-authority.ts
|
|
4207
|
+
function membershipCanRestart(member, now) {
|
|
4208
|
+
const end = member.status === "payment_grace" ? Number(member.graceEndsAt) : Number(member.termEndsAt);
|
|
4209
|
+
return Number.isSafeInteger(member.memberNumber) && Number(member.memberNumber) > 0 && Number.isSafeInteger(member.journeyApprovedAt) && (["refunded", "canceled", "lapsed"].includes(String(member.status)) || ["active", "cancel_at_term_end", "payment_grace"].includes(String(member.status)) && Number.isSafeInteger(end) && end <= now);
|
|
4210
|
+
}
|
|
4211
|
+
async function stopPreviousSubscription(member, target, secret) {
|
|
4212
|
+
const path = `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`;
|
|
4213
|
+
const result = await stripeRequest5({ secretKey: secret }, "GET", path);
|
|
4214
|
+
const sub = result.body;
|
|
4215
|
+
if (!result.ok || sub.id !== member.providerSubscriptionId || sub.customer !== member.providerCustomerId || sub.livemode !== (target.environment === "prod") || !Object.entries({
|
|
4216
|
+
...stripePartitionMetadata(target),
|
|
4217
|
+
applicationId: target.applicationId
|
|
4218
|
+
}).every(([key3, value]) => sub.metadata?.[key3] === value)) membershipFail("membership_restart_provider_mismatch");
|
|
4219
|
+
if (sub.status === "canceled") return;
|
|
4220
|
+
const canceled = await stripeRequest5(
|
|
4221
|
+
{ secretKey: secret },
|
|
4222
|
+
"DELETE",
|
|
4223
|
+
path,
|
|
4224
|
+
{ invoice_now: false, prorate: false },
|
|
4225
|
+
{ idempotencyKey: `membership-restart-cancel:${member.providerSubscriptionId}` }
|
|
4226
|
+
);
|
|
4227
|
+
if (!canceled.ok || canceled.body.id !== member.providerSubscriptionId || canceled.body.status !== "canceled") membershipFail("membership_restart_cancellation_pending");
|
|
4228
|
+
}
|
|
4229
|
+
async function prepareMembershipRestart(db, input, now, stripeSecret) {
|
|
4230
|
+
membershipClock(now);
|
|
4231
|
+
if (!/^sub_[A-Za-z0-9_]+$/.test(input.previousSubscriptionId)) membershipFail("membership_restart_conflict");
|
|
4232
|
+
const id10 = await networkMembershipId(input), bindingId = await membershipCheckoutBindingId(input);
|
|
4233
|
+
const restartId = `restart_${await stripePayloadDigest(JSON.stringify([id10, input.previousSubscriptionId]))}`;
|
|
4234
|
+
const state = await loadMembershipAuthorityState(db, input);
|
|
4235
|
+
const previous = await membershipLoad(db, "networkMembership", id10);
|
|
4236
|
+
requireMembershipScope(previous, input);
|
|
4237
|
+
const receipt = {
|
|
4238
|
+
applicationId: input.applicationId,
|
|
4239
|
+
membershipId: id10,
|
|
4240
|
+
restartId,
|
|
4241
|
+
memberNumber: Number(previous.memberNumber),
|
|
4242
|
+
status: "ready"
|
|
4243
|
+
};
|
|
4244
|
+
const existing = await membershipLoad(db, "membershipRestart", id10);
|
|
4245
|
+
if (existing?.restartId === restartId && existing.status === "ready") {
|
|
4246
|
+
requireMembershipScope(existing, input);
|
|
4247
|
+
return receipt;
|
|
4248
|
+
}
|
|
4249
|
+
if (previous.providerSubscriptionId !== input.previousSubscriptionId) membershipFail("membership_restart_conflict");
|
|
4250
|
+
if (previous.status !== "restarting") {
|
|
4251
|
+
if (!membershipCanRestart(previous, now)) membershipFail("membership_restart_not_eligible");
|
|
4252
|
+
const binding2 = await membershipLoad(db, "membershipOfferCheckout", bindingId);
|
|
4253
|
+
requireMembershipScope(binding2, input);
|
|
4254
|
+
const seats = await expireRestartedMembershipSeat(db, input, previous, now);
|
|
4255
|
+
await db.transact([
|
|
4256
|
+
...seats.ops,
|
|
4257
|
+
membershipUpdate("membershipPurchaseHistory", restartId, {
|
|
4258
|
+
id: restartId,
|
|
4259
|
+
...membershipScope(input),
|
|
4260
|
+
membershipId: id10,
|
|
4261
|
+
providerSubscriptionId: input.previousSubscriptionId,
|
|
4262
|
+
memberSnapshot: previous,
|
|
4263
|
+
bindingSnapshot: binding2,
|
|
4264
|
+
archivedAt: now
|
|
4265
|
+
}),
|
|
4266
|
+
membershipUpdate("networkMembership", id10, { status: "restarting", restartId, revision: Number(previous.revision) + 1, updatedAt: now })
|
|
4267
|
+
], { mutationId: `membership-restart-reserve:${restartId}`, guards: [
|
|
4268
|
+
...seats.guards,
|
|
4269
|
+
{ ns: "networkMembership", id: id10, exists: true, equals: { revision: previous.revision, status: previous.status } },
|
|
4270
|
+
{ ns: "membershipPurchaseHistory", id: restartId, exists: false },
|
|
4271
|
+
{ ns: "membershipOfferCheckout", id: bindingId, exists: true, equals: { offerDigest: binding2.offerDigest } },
|
|
4272
|
+
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
|
|
4273
|
+
] });
|
|
4274
|
+
} else if (previous.restartId !== restartId) membershipFail("membership_restart_conflict");
|
|
4275
|
+
const reserved = await membershipLoad(db, "networkMembership", id10);
|
|
4276
|
+
requireMembershipScope(reserved, input);
|
|
4277
|
+
if (reserved.status !== "restarting" || reserved.restartId !== restartId) membershipFail("membership_restart_conflict");
|
|
4278
|
+
await stopPreviousSubscription(reserved, input, stripeSecret);
|
|
4279
|
+
const binding = await membershipLoad(db, "membershipOfferCheckout", bindingId);
|
|
4280
|
+
requireMembershipScope(binding, input);
|
|
4281
|
+
await db.transact([
|
|
4282
|
+
membershipUpdate("membershipRestart", id10, { id: id10, ...membershipScope(input), ...receipt, previousSubscriptionId: input.previousSubscriptionId, createdAt: now }),
|
|
4283
|
+
{ t: "delete", ns: "membershipOfferCheckout", id: bindingId }
|
|
4284
|
+
], { mutationId: `membership-restart-ready:${restartId}`, guards: [
|
|
4285
|
+
{ ns: "membershipRestart", id: id10, exists: Boolean(existing), ...existing ? { equals: { restartId: existing.restartId, status: existing.status } } : {} },
|
|
4286
|
+
{ ns: "networkMembership", id: id10, exists: true, equals: { revision: reserved.revision, status: "restarting", restartId } },
|
|
4287
|
+
{ ns: "membershipOfferCheckout", id: bindingId, exists: true, equals: { offerDigest: binding.offerDigest } }
|
|
4288
|
+
] });
|
|
4289
|
+
return receipt;
|
|
4290
|
+
}
|
|
4291
|
+
|
|
3970
4292
|
// src/membership-access-authority.ts
|
|
3971
4293
|
async function readMembershipAccess(db, input, now) {
|
|
3972
4294
|
membershipClock(now);
|
|
@@ -3983,7 +4305,8 @@ async function readMembershipAccess(db, input, now) {
|
|
|
3983
4305
|
allowed: allowed2,
|
|
3984
4306
|
status: String(member.status),
|
|
3985
4307
|
accessEndsAt: Number.isSafeInteger(endsAt) ? endsAt : 0,
|
|
3986
|
-
revision: Number(member.revision)
|
|
4308
|
+
revision: Number(member.revision),
|
|
4309
|
+
restartEligible: !allowed2 && membershipCanRestart(member, now)
|
|
3987
4310
|
};
|
|
3988
4311
|
}
|
|
3989
4312
|
async function verifyMembershipAccessReceipt(value, input, now) {
|
|
@@ -3991,7 +4314,7 @@ async function verifyMembershipAccessReceipt(value, input, now) {
|
|
|
3991
4314
|
if (!row2 || row2.membershipId !== await networkMembershipId(input) || typeof row2.allowed !== "boolean" || typeof row2.status !== "string" || !Number.isSafeInteger(row2.revision) || row2.revision < 0 || !Number.isSafeInteger(row2.accessEndsAt) || row2.accessEndsAt < 0 || row2.allowed && (!["active", "cancel_at_term_end", "payment_grace"].includes(row2.status) || row2.accessEndsAt <= now || row2.revision < 1)) {
|
|
3992
4315
|
throw new Error("invalid network access receipt");
|
|
3993
4316
|
}
|
|
3994
|
-
return { membershipId: row2.membershipId, allowed: row2.allowed, status: row2.status, accessEndsAt: row2.accessEndsAt, revision: row2.revision };
|
|
4317
|
+
return { membershipId: row2.membershipId, allowed: row2.allowed, status: row2.status, accessEndsAt: row2.accessEndsAt, revision: row2.revision, ...row2.restartEligible === true && !row2.allowed ? { restartEligible: true } : {} };
|
|
3995
4318
|
}
|
|
3996
4319
|
|
|
3997
4320
|
// src/membership-seat-reservation.ts
|
|
@@ -4072,16 +4395,16 @@ async function reserveNamedMembershipSeat(db, input, now) {
|
|
|
4072
4395
|
}
|
|
4073
4396
|
|
|
4074
4397
|
// src/membership-seat-stripe.ts
|
|
4075
|
-
import { stripeRequest as
|
|
4398
|
+
import { stripeRequest as stripeRequest7 } from "@odla-ai/stripe";
|
|
4076
4399
|
|
|
4077
4400
|
// src/subscription-provider-contract.ts
|
|
4078
|
-
import { stripeRequest as
|
|
4401
|
+
import { stripeRequest as stripeRequest6 } from "@odla-ai/stripe";
|
|
4079
4402
|
function subscriptionContractMismatch() {
|
|
4080
4403
|
throw Object.assign(new Error("Stripe subscription does not match the checkout contract"), { code: "stripe_contract_mismatch" });
|
|
4081
4404
|
}
|
|
4082
4405
|
async function verifySubscriptionPrice(secretKey, input, resume = false) {
|
|
4083
4406
|
if (!/^price_[A-Za-z0-9]+$/.test(input.priceId) || !Number.isSafeInteger(input.priceCents) || input.priceCents <= 0 || !/^[a-z]{3}$/.test(input.currency) || !["month", "year"].includes(input.interval)) subscriptionContractMismatch();
|
|
4084
|
-
const result = await
|
|
4407
|
+
const result = await stripeRequest6({ secretKey }, "GET", `/v1/prices/${encodeURIComponent(input.priceId)}`);
|
|
4085
4408
|
const price = result.body;
|
|
4086
4409
|
if (!result.ok || !price || price.id !== input.priceId || !resume && price.active !== true || price.unit_amount !== input.priceCents || price.currency !== input.currency || price.type !== "recurring" || price.billing_scheme !== "per_unit" || price.custom_unit_amount || price.transform_quantity || price.recurring?.interval !== input.interval || price.recurring?.interval_count !== 1 || price.recurring?.usage_type !== "licensed" || price.livemode !== (input.environment === "prod")) subscriptionContractMismatch();
|
|
4087
4410
|
return typeof price.product === "string" ? price.product : void 0;
|
|
@@ -4099,7 +4422,7 @@ var metadataFor = (seat) => ({
|
|
|
4099
4422
|
});
|
|
4100
4423
|
var matches = (value, expected) => value && Object.entries(expected).every(([key3, item]) => value[key3] === item);
|
|
4101
4424
|
async function read(sk, path, params) {
|
|
4102
|
-
const response = await
|
|
4425
|
+
const response = await stripeRequest7({ secretKey: sk }, "GET", path, params);
|
|
4103
4426
|
if (!response.ok) return reject3();
|
|
4104
4427
|
return response.body;
|
|
4105
4428
|
}
|
|
@@ -4136,11 +4459,11 @@ async function createNamedSeatStripePayment(sk, seat, now) {
|
|
|
4136
4459
|
}
|
|
4137
4460
|
await subscriptionForSeat(sk, seat);
|
|
4138
4461
|
const metadata = metadataFor(seat);
|
|
4139
|
-
const result = seat.providerPaymentIntentId ? await
|
|
4462
|
+
const result = seat.providerPaymentIntentId ? await stripeRequest7(
|
|
4140
4463
|
{ secretKey: sk },
|
|
4141
4464
|
"GET",
|
|
4142
4465
|
`/v1/payment_intents/${encodeURIComponent(seat.providerPaymentIntentId)}`
|
|
4143
|
-
) : await
|
|
4466
|
+
) : await stripeRequest7({ secretKey: sk }, "POST", "/v1/payment_intents", {
|
|
4144
4467
|
amount: seat.amountCents,
|
|
4145
4468
|
currency: seat.currency,
|
|
4146
4469
|
customer: seat.providerCustomerId,
|
|
@@ -4158,7 +4481,7 @@ async function attachNamedSeatStripeRenewal(sk, seat) {
|
|
|
4158
4481
|
const sub = await subscriptionForSeat(sk, seat);
|
|
4159
4482
|
const existing = sub.items.data.find((item2) => item2.metadata?.seatId === seat.id);
|
|
4160
4483
|
if (existing) return String(existing.id);
|
|
4161
|
-
const result = await
|
|
4484
|
+
const result = await stripeRequest7({ secretKey: sk }, "POST", "/v1/subscription_items", {
|
|
4162
4485
|
subscription: seat.providerSubscriptionId,
|
|
4163
4486
|
price: seat.priceId,
|
|
4164
4487
|
quantity: 1,
|
|
@@ -4236,8 +4559,7 @@ async function confirmNamedMembershipSeatPayment(db, input, now) {
|
|
|
4236
4559
|
requireMembershipScope(member, input);
|
|
4237
4560
|
const row2 = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
|
|
4238
4561
|
if (!row2 || !seat.providerPaymentIntentId) membershipFail("named_seat_payment_evidence_pending");
|
|
4239
|
-
const
|
|
4240
|
-
if (await membershipLoad(db, "networkCommercialExceptions", collisionId)) membershipFail("named_seat_payment_evidence_quarantined");
|
|
4562
|
+
const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, row2.digest, "named_seat_payment_evidence_quarantined");
|
|
4241
4563
|
const fact = row2.fact;
|
|
4242
4564
|
if (!fact || fact.eventId !== input.eventId || fact.digest !== row2.digest || !/^[a-f0-9]{64}$/.test(fact.digest) || fact.eventType !== "payment_intent.succeeded" || fact.objectKind !== "payment_intent" || fact.status !== "succeeded" || fact.livemode !== (input.environment === "prod") || fact.paymentIntentId !== seat.providerPaymentIntentId || fact.seatId !== seat.id || fact.membershipId !== member.id || fact.customerId !== seat.providerCustomerId || fact.applicationId !== input.applicationId || fact.amountCents !== seat.amountCents || fact.amountReceivedCents !== seat.amountCents || fact.currency !== seat.currency || !fact.identity || Object.entries({
|
|
4243
4565
|
appId: input.appId,
|
|
@@ -4284,7 +4606,7 @@ async function confirmNamedMembershipSeatPayment(db, input, now) {
|
|
|
4284
4606
|
{ ns: "networkMembership", id: String(member.id), exists: true, equals: { revision: member.revision } },
|
|
4285
4607
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
|
|
4286
4608
|
{ ns: "networkCommercialEvents", id: String(row2.id), exists: true, equals: { digest: fact.digest } },
|
|
4287
|
-
|
|
4609
|
+
evidenceGuard
|
|
4288
4610
|
] });
|
|
4289
4611
|
} catch (error) {
|
|
4290
4612
|
if (error.code !== "transact_guard_failed") throw error;
|
|
@@ -4433,14 +4755,14 @@ async function recordRecipient(db, input, now, approve) {
|
|
|
4433
4755
|
}
|
|
4434
4756
|
|
|
4435
4757
|
// src/membership-seat-refund-stripe.ts
|
|
4436
|
-
import { stripeRequest as
|
|
4758
|
+
import { stripeRequest as stripeRequest8 } from "@odla-ai/stripe";
|
|
4437
4759
|
var fail4 = (code = "named_seat_refund_mismatch") => {
|
|
4438
4760
|
throw Object.assign(new Error(code), { code });
|
|
4439
4761
|
};
|
|
4440
4762
|
async function refundNamedSeatStripePayment(secretKey, seat) {
|
|
4441
4763
|
if (!seat.providerPaymentIntentId || seat.status !== "refund_pending") fail4();
|
|
4442
4764
|
const call = async (method, path, params, idempotencyKey) => {
|
|
4443
|
-
const result = await
|
|
4765
|
+
const result = await stripeRequest8({ secretKey }, method, path, params, idempotencyKey ? { idempotencyKey } : {});
|
|
4444
4766
|
if (!result.ok || !result.body) return fail4("named_seat_refund_provider_unavailable");
|
|
4445
4767
|
return result.body;
|
|
4446
4768
|
};
|
|
@@ -4562,7 +4884,7 @@ async function confirmNamedSeatProviderRefund(db, input, now) {
|
|
|
4562
4884
|
const row2 = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
|
|
4563
4885
|
if (!row2) return membershipFail("named_seat_refund_evidence_pending");
|
|
4564
4886
|
const fact = row2.fact;
|
|
4565
|
-
|
|
4887
|
+
await commercialEvidenceGuard(db, input.eventId, row2.digest, "named_seat_refund_evidence_quarantined");
|
|
4566
4888
|
if (!fact || fact.eventId !== input.eventId || fact.digest !== row2.digest || !/^[a-f0-9]{64}$/.test(fact.digest) || fact.eventType !== "charge.refunded" || fact.seatId !== seat.id || fact.membershipId !== seat.primaryMembershipId || fact.paymentIntentId !== seat.providerPaymentIntentId || fact.amountCents !== seat.amountCents || fact.currency !== seat.currency || fact.customerId !== seat.providerCustomerId || fact.applicationId !== input.applicationId || fact.livemode !== (input.environment === "prod") || !fact.identity || Object.entries({ appId: input.appId, chapterId: input.chapterId, environment: input.environment, dataEnvironment: input.environment, runtime: input.runtime }).some(([key3, value]) => fact.identity[key3] !== value)) membershipFail("named_seat_refund_evidence_mismatch");
|
|
4567
4889
|
await requestNamedSeatRefund(db, { ...input, reason: "provider_refund" }, now);
|
|
4568
4890
|
}
|
|
@@ -4834,7 +5156,7 @@ function createMembershipAuthorityHandler(options) {
|
|
|
4834
5156
|
if (req.method !== "POST") return respond({ error: "method_not_allowed" }, 405);
|
|
4835
5157
|
const sender = req.headers.get(FEDERATION_HEADERS.sender);
|
|
4836
5158
|
const action = url.pathname.split("/").at(-1);
|
|
4837
|
-
if (!["offers", "checkouts", "payments", "approvals", "refunds", "renewals", "access", ...Object.keys(MEMBERSHIP_SEAT_ACTION_FIELDS)].includes(action ?? "")) return respond({ error: "unauthorized" }, 401);
|
|
5159
|
+
if (!["offers", "checkouts", "payments", "approvals", "refunds", "renewals", "access", "restarts", "cancel-renewal", ...Object.keys(MEMBERSHIP_SEAT_ACTION_FIELDS)].includes(action ?? "")) return respond({ error: "unauthorized" }, 401);
|
|
4838
5160
|
const peer = peers.find((candidate) => candidate.appId === sender && url.pathname === `${MEMBERSHIP_AUTHORITY_PATH}/${candidate.runtime}/${action}` && !url.search);
|
|
4839
5161
|
if (!peer) return respond({ error: "unauthorized" }, 401);
|
|
4840
5162
|
let body;
|
|
@@ -4856,7 +5178,7 @@ function createMembershipAuthorityHandler(options) {
|
|
|
4856
5178
|
let input;
|
|
4857
5179
|
try {
|
|
4858
5180
|
input = JSON.parse(verified.body);
|
|
4859
|
-
const fields = action === "seat-invitation" ? MEMBERSHIP_SEAT_ACTION_FIELDS[action] : ["applicationId", "skuId", ...action === "offers" ? ["requestId"] : action === "checkouts" ? ["offerId", "offerDigest", "quoteDigest", "priceId"] : action === "payments" ? ["eventId", "providerSubscriptionId", "providerCustomerId"] : action === "renewals" ? ["eventId"] : MEMBERSHIP_SEAT_ACTION_FIELDS[action] ?? []];
|
|
5181
|
+
const fields = action === "seat-invitation" ? MEMBERSHIP_SEAT_ACTION_FIELDS[action] : ["applicationId", "skuId", ...action === "offers" ? ["requestId"] : action === "checkouts" ? ["offerId", "offerDigest", "quoteDigest", "priceId"] : action === "payments" ? ["eventId", "providerSubscriptionId", "providerCustomerId"] : action === "renewals" ? ["eventId"] : action === "restarts" ? ["previousSubscriptionId"] : MEMBERSHIP_SEAT_ACTION_FIELDS[action] ?? []];
|
|
4860
5182
|
if (!input || typeof input !== "object" || Array.isArray(input) || Object.keys(input).some((key3) => !fields.includes(key3)) || !fields.every((key3) => typeof input[key3] === "string" && (["recipientName", "recipientEmail", "purchaserEmail", "verifiedRecipientEmail"].includes(key3) ? input[key3].length > 0 && input[key3].length <= 254 && !/[\u0000-\u001f\u007f]/.test(input[key3]) : SAFE.test(input[key3])))) {
|
|
4861
5183
|
throw new TypeError("invalid request");
|
|
4862
5184
|
}
|
|
@@ -4881,6 +5203,15 @@ function createMembershipAuthorityHandler(options) {
|
|
|
4881
5203
|
applicationId: String(input.applicationId),
|
|
4882
5204
|
skuId: String(input.skuId)
|
|
4883
5205
|
};
|
|
5206
|
+
if (action === "restarts" || action === "cancel-renewal") {
|
|
5207
|
+
const stripeSecret = await getVaultSecret(db, options.stripeSecretName ?? "stripe_secret_key");
|
|
5208
|
+
if (!stripeSecret) return respond({ error: "membership_authority_unavailable" }, 503);
|
|
5209
|
+
if (action === "cancel-renewal") return respond({ ok: true, renewal: await cancelMembershipAtTermEnd(db, target, options.now?.() ?? Date.now(), stripeSecret) });
|
|
5210
|
+
return respond({ ok: true, restart: await prepareMembershipRestart(db, {
|
|
5211
|
+
...target,
|
|
5212
|
+
previousSubscriptionId: String(input.previousSubscriptionId)
|
|
5213
|
+
}, options.now?.() ?? Date.now(), stripeSecret) });
|
|
5214
|
+
}
|
|
4884
5215
|
if (action === "access") return respond({ ok: true, access: await readMembershipAccess(db, target, options.now?.() ?? Date.now()) });
|
|
4885
5216
|
if (action === "renewals") return respond({ ok: true, renewal: await reconcileMembershipBilling(
|
|
4886
5217
|
db,
|
|
@@ -5154,7 +5485,7 @@ async function requestMembershipRefund(options) {
|
|
|
5154
5485
|
async function requestMembershipRenewal(options) {
|
|
5155
5486
|
const result = await requestAuthority(options, "renewals", { applicationId: options.applicationId, skuId: options.skuId, eventId: options.eventId });
|
|
5156
5487
|
const row2 = result.renewal;
|
|
5157
|
-
if (!row2 || row2.membershipId !== await networkMembershipId(options) || !Number.isSafeInteger(row2.termEndsAt) || row2.termEndsAt <= 0 || !Number.isSafeInteger(row2.graceEndsAt) || row2.graceEndsAt < 0 || typeof row2.autoRenew !== "boolean" || !["active", "paid_pending_approval", "cancel_at_term_end", "payment_grace", "lapsed", "refund_pending", "refunded", "canceled", "declined"].includes(row2.status)) {
|
|
5488
|
+
if (!row2 || row2.membershipId !== await networkMembershipId(options) || !Number.isSafeInteger(row2.termEndsAt) || row2.termEndsAt <= 0 || !Number.isSafeInteger(row2.graceEndsAt) || row2.graceEndsAt < 0 || typeof row2.autoRenew !== "boolean" || !["active", "paid_pending_approval", "cancel_at_term_end", "payment_grace", "lapsed", "refund_pending", "refunded", "canceled", "declined", "restarting"].includes(row2.status)) {
|
|
5158
5489
|
throw new Error("invalid membership renewal receipt");
|
|
5159
5490
|
}
|
|
5160
5491
|
return { membershipId: row2.membershipId, status: row2.status, termEndsAt: row2.termEndsAt, graceEndsAt: row2.graceEndsAt, autoRenew: row2.autoRenew };
|
|
@@ -5196,9 +5527,25 @@ async function requestMembershipAccess(options) {
|
|
|
5196
5527
|
const result = await requestAuthority(options, "access", { applicationId: options.applicationId, skuId: options.skuId });
|
|
5197
5528
|
return verifyMembershipAccessReceipt(result.access, options, Date.now());
|
|
5198
5529
|
}
|
|
5530
|
+
async function requestMembershipRestart(options) {
|
|
5531
|
+
const result = await requestAuthority(options, "restarts", {
|
|
5532
|
+
applicationId: options.applicationId,
|
|
5533
|
+
skuId: options.skuId,
|
|
5534
|
+
previousSubscriptionId: options.previousSubscriptionId
|
|
5535
|
+
});
|
|
5536
|
+
const row2 = result.restart;
|
|
5537
|
+
if (!row2 || row2.applicationId !== options.applicationId || row2.status !== "ready" || row2.membershipId !== await networkMembershipId(options) || !/^restart_[a-f0-9]{64}$/.test(row2.restartId) || !Number.isSafeInteger(row2.memberNumber) || row2.memberNumber < 1) throw new Error("invalid membership restart receipt");
|
|
5538
|
+
return { applicationId: row2.applicationId, membershipId: row2.membershipId, restartId: row2.restartId, memberNumber: row2.memberNumber, status: "ready" };
|
|
5539
|
+
}
|
|
5540
|
+
async function requestMembershipCancellation(options) {
|
|
5541
|
+
const result = await requestAuthority(options, "cancel-renewal", { applicationId: options.applicationId, skuId: options.skuId });
|
|
5542
|
+
const row2 = result.renewal;
|
|
5543
|
+
if (!row2 || row2.membershipId !== await networkMembershipId(options) || row2.autoRenew !== false || row2.graceEndsAt !== 0 || !Number.isSafeInteger(row2.termEndsAt) || row2.termEndsAt <= 0 || !["cancel_at_term_end", "lapsed", "paid_pending_approval"].includes(row2.status)) throw new Error("invalid membership cancellation receipt");
|
|
5544
|
+
return { membershipId: row2.membershipId, status: row2.status, termEndsAt: row2.termEndsAt, graceEndsAt: 0, autoRenew: false };
|
|
5545
|
+
}
|
|
5199
5546
|
|
|
5200
5547
|
// src/membership-seat-cancel.ts
|
|
5201
|
-
import { initStripe, stripeRequest as
|
|
5548
|
+
import { initStripe, stripeRequest as stripeRequest9 } from "@odla-ai/stripe";
|
|
5202
5549
|
async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
|
|
5203
5550
|
membershipClock(now);
|
|
5204
5551
|
const state = await loadMembershipAuthorityState(db, target);
|
|
@@ -5226,7 +5573,7 @@ async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
|
|
|
5226
5573
|
}
|
|
5227
5574
|
const metadata = { ...stripePartitionMetadata(target), applicationId: target.applicationId, membershipId: seat.primaryMembershipId, seatId: seat.id };
|
|
5228
5575
|
const request = async (method, path, params) => {
|
|
5229
|
-
const result = await
|
|
5576
|
+
const result = await stripeRequest9(
|
|
5230
5577
|
{ secretKey },
|
|
5231
5578
|
method,
|
|
5232
5579
|
path,
|
|
@@ -5463,7 +5810,27 @@ var membershipAuthorityEntities = {
|
|
|
5463
5810
|
cancellationRequestedAt: attr9("number", true),
|
|
5464
5811
|
graceEndsAt: attr9("number", true),
|
|
5465
5812
|
...times(),
|
|
5466
|
-
refundRequestedAt: attr9("number", true)
|
|
5813
|
+
refundRequestedAt: attr9("number", true),
|
|
5814
|
+
restartId: key2(true)
|
|
5815
|
+
} },
|
|
5816
|
+
membershipRestart: { attrs: {
|
|
5817
|
+
id: id9(),
|
|
5818
|
+
...scope(),
|
|
5819
|
+
membershipId: key2(),
|
|
5820
|
+
restartId: key2(),
|
|
5821
|
+
previousSubscriptionId: key2(),
|
|
5822
|
+
memberNumber: attr9("number"),
|
|
5823
|
+
status: key2(),
|
|
5824
|
+
createdAt: attr9("number")
|
|
5825
|
+
} },
|
|
5826
|
+
membershipPurchaseHistory: { attrs: {
|
|
5827
|
+
id: id9(),
|
|
5828
|
+
...scope(),
|
|
5829
|
+
membershipId: key2(),
|
|
5830
|
+
providerSubscriptionId: key2(),
|
|
5831
|
+
memberSnapshot: attr9("json"),
|
|
5832
|
+
bindingSnapshot: attr9("json"),
|
|
5833
|
+
archivedAt: attr9("number")
|
|
5467
5834
|
} },
|
|
5468
5835
|
networkEntitlement: { attrs: {
|
|
5469
5836
|
id: id9(),
|
|
@@ -6894,9 +7261,9 @@ function sharedRecordFromCrm(crm, record4, target, sourceId) {
|
|
|
6894
7261
|
}
|
|
6895
7262
|
const nameField = def.nameField ?? "name";
|
|
6896
7263
|
const input = {};
|
|
6897
|
-
for (const
|
|
6898
|
-
const value = record4.fields?.[
|
|
6899
|
-
if (value !== void 0) input[
|
|
7264
|
+
for (const field2 of /* @__PURE__ */ new Set([nameField, ...fields])) {
|
|
7265
|
+
const value = record4.fields?.[field2];
|
|
7266
|
+
if (value !== void 0) input[field2] = value;
|
|
6900
7267
|
}
|
|
6901
7268
|
if (input[nameField] === void 0) input[nameField] = record4.name;
|
|
6902
7269
|
return sourceId ? { version: 2, source: { siteId: sourceId, recordId: record4.id }, type: record4.type, input } : { version: 1, type: record4.type, hubRecordId: record4.id, input };
|
|
@@ -7165,9 +7532,9 @@ function subAnnualCents(sub) {
|
|
|
7165
7532
|
}
|
|
7166
7533
|
|
|
7167
7534
|
// src/payments-stripe.ts
|
|
7168
|
-
import { stripeRequest as
|
|
7535
|
+
import { stripeRequest as stripeRequest10, verifyStripeSignature as verifyStripeSignature2 } from "@odla-ai/stripe";
|
|
7169
7536
|
async function stripeCall(sk, method, path, params, idempotencyKey) {
|
|
7170
|
-
return
|
|
7537
|
+
return stripeRequest10(
|
|
7171
7538
|
{ secretKey: sk },
|
|
7172
7539
|
method,
|
|
7173
7540
|
path,
|
|
@@ -7477,7 +7844,8 @@ function applicationSummary(app) {
|
|
|
7477
7844
|
status: app.status,
|
|
7478
7845
|
createdAt: app.createdAt ?? null,
|
|
7479
7846
|
meetingLink: app.meetingLink ?? null,
|
|
7480
|
-
paid: Boolean(app.stripeSubscriptionId) && app.status !== "refunded",
|
|
7847
|
+
paid: Boolean(app.stripeSubscriptionId) && app.status !== "refunded" && app.canceled !== true && !["lapsed", "refunded", "refund_pending", "canceled", "restarting"].includes(app.membershipStatus ?? ""),
|
|
7848
|
+
...app.membershipStatus ? { membershipStatus: app.membershipStatus, membershipGraceEndsAt: app.membershipGraceEndsAt, membershipAutoRenew: app.membershipAutoRenew } : {},
|
|
7481
7849
|
renewalAt: app.renewalAt ?? null,
|
|
7482
7850
|
canceled: app.canceled === true,
|
|
7483
7851
|
tierId: app.tierId ?? null
|
|
@@ -7659,7 +8027,7 @@ function isValidTimeZone(tz) {
|
|
|
7659
8027
|
function resolveScheduling(config) {
|
|
7660
8028
|
const result = validateScheduling(config);
|
|
7661
8029
|
if (result.ok) return result.value;
|
|
7662
|
-
const detail = Object.entries(result.errors).map(([
|
|
8030
|
+
const detail = Object.entries(result.errors).map(([field2, message]) => `${field2}: ${message}`).join(" ");
|
|
7663
8031
|
throw new Error(`scheduling: ${detail}`);
|
|
7664
8032
|
}
|
|
7665
8033
|
function validateScheduling(config) {
|
|
@@ -8806,7 +9174,7 @@ var MEMBER_SELF_READABLE_FIELDS = new Set(
|
|
|
8806
9174
|
MEMBER_SYSTEM_AUTHORITY_MATRIX.filter((row2) => row2.memberAccess !== "none").map((row2) => row2.id)
|
|
8807
9175
|
);
|
|
8808
9176
|
function projectMemberSelf(record4) {
|
|
8809
|
-
return Object.fromEntries(Object.entries(record4).filter(([
|
|
9177
|
+
return Object.fromEntries(Object.entries(record4).filter(([field2]) => MEMBER_SELF_READABLE_FIELDS.has(field2)));
|
|
8810
9178
|
}
|
|
8811
9179
|
function classifyMemberSystemWrite(intent) {
|
|
8812
9180
|
if (intent.actor === "bnf") return "reject_bnf_surface";
|
|
@@ -9160,12 +9528,14 @@ export {
|
|
|
9160
9528
|
renewalPatch,
|
|
9161
9529
|
requestMembershipAccess,
|
|
9162
9530
|
requestMembershipApproval,
|
|
9531
|
+
requestMembershipCancellation,
|
|
9163
9532
|
requestMembershipChapterEffect,
|
|
9164
9533
|
requestMembershipCheckoutBinding,
|
|
9165
9534
|
requestMembershipCommercialOffer,
|
|
9166
9535
|
requestMembershipPayment,
|
|
9167
9536
|
requestMembershipRefund,
|
|
9168
9537
|
requestMembershipRenewal,
|
|
9538
|
+
requestMembershipRestart,
|
|
9169
9539
|
requestNamedSeatAcceptance,
|
|
9170
9540
|
requestNamedSeatAccess,
|
|
9171
9541
|
requestNamedSeatApproval,
|