@odla-ai/chapter 0.48.3 → 0.50.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/README.md +29 -2
- package/dist/{chunk-MQ4OPZCS.js → chunk-4EA5V4JE.js} +994 -648
- package/dist/chunk-4EA5V4JE.js.map +1 -0
- package/dist/{chunk-4HS2JPE2.js → chunk-JXX4BFDP.js} +21 -2
- package/dist/chunk-JXX4BFDP.js.map +1 -0
- package/dist/{chunk-KZ2AXEZW.js → chunk-OS46ZE46.js} +765 -543
- package/dist/chunk-OS46ZE46.js.map +1 -0
- package/dist/{copy-context-CMupQL9h.d.ts → copy-context-Ddat__H5.d.ts} +16 -3
- package/dist/index.cjs +591 -158
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +101 -8
- package/dist/index.d.ts +101 -8
- package/dist/index.js +587 -154
- package/dist/index.js.map +1 -1
- package/dist/ui/admin/index.d.ts +64 -5
- package/dist/ui/admin/index.js +14 -2
- package/dist/ui/index.d.ts +3 -3
- package/dist/ui/index.js +19 -3
- package/dist/ui/member/index.d.ts +44 -4
- package/dist/ui/member/index.js +6 -2
- package/dist/worker/index.cjs +5114 -4064
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +105 -6
- package/dist/worker/index.d.ts +105 -6
- package/dist/worker/index.js +5104 -4054
- 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;
|
|
@@ -1394,6 +1442,7 @@ var DEFAULT_ADMIN_COPY = {
|
|
|
1394
1442
|
},
|
|
1395
1443
|
records: {
|
|
1396
1444
|
workflowMissing: "This record is not linked to an application workflow.",
|
|
1445
|
+
invitation: "Invitation",
|
|
1397
1446
|
comms: "Comms",
|
|
1398
1447
|
commsHistory: "Comms history",
|
|
1399
1448
|
scheduling: "Scheduling",
|
|
@@ -1441,6 +1490,19 @@ var DEFAULT_COMMON_COPY = {
|
|
|
1441
1490
|
unknown: "(unknown)"
|
|
1442
1491
|
};
|
|
1443
1492
|
var DEFAULT_JOIN_COPY = {
|
|
1493
|
+
namedSeat: {
|
|
1494
|
+
title: "Your membership invitation",
|
|
1495
|
+
from: "{purchaserName} invited you.",
|
|
1496
|
+
prepaid: "Your {tierName} seat is already paid for.",
|
|
1497
|
+
body: "Accept your invitation, then complete your own application requirements. You will not be asked to pay for this seat.",
|
|
1498
|
+
consent: "I accept this membership for myself. It cannot be transferred and remains current while the purchaser's membership and my seat are current.",
|
|
1499
|
+
accept: "Accept my invitation",
|
|
1500
|
+
accepting: "Confirming your invitation\u2026",
|
|
1501
|
+
loading: "Loading your invitation\u2026",
|
|
1502
|
+
unavailable: "Your invitation is unavailable. Sign in with the invited email address, then try again.",
|
|
1503
|
+
retry: "Try again",
|
|
1504
|
+
failure: "We could not confirm acceptance. Retry this invitation; you do not need to purchase another membership."
|
|
1505
|
+
},
|
|
1444
1506
|
form: {
|
|
1445
1507
|
submit: "Submit application",
|
|
1446
1508
|
submitting: "Submitting\u2026",
|
|
@@ -1448,6 +1510,8 @@ var DEFAULT_JOIN_COPY = {
|
|
|
1448
1510
|
unexpectedFailure: "Something went wrong. Please try again."
|
|
1449
1511
|
},
|
|
1450
1512
|
booking: {
|
|
1513
|
+
title: "Book your introduction",
|
|
1514
|
+
intro: "Choose a time to meet your chapter and get to know each other.",
|
|
1451
1515
|
unavailable: "Scheduling is briefly unavailable. We'll reach out by email to arrange your call.",
|
|
1452
1516
|
loadFailed: "Times are briefly unavailable. Please try again.",
|
|
1453
1517
|
slotTaken: "That time was just taken. Here are the current openings.",
|
|
@@ -1463,7 +1527,9 @@ var DEFAULT_JOIN_COPY = {
|
|
|
1463
1527
|
processing: "Processing\u2026",
|
|
1464
1528
|
payAndContinue: "Pay and continue",
|
|
1465
1529
|
incomplete: "The payment did not complete. Please try again.",
|
|
1466
|
-
consent: "I have read and agree to the pricing and refund policy above."
|
|
1530
|
+
consent: "I have read and agree to the pricing and refund policy above.",
|
|
1531
|
+
foundingMember: "Founding member rate",
|
|
1532
|
+
foundingDiscount: "Founding discount"
|
|
1467
1533
|
},
|
|
1468
1534
|
done: {
|
|
1469
1535
|
label: "You're booked",
|
|
@@ -1639,16 +1705,16 @@ function resolveFormationFlowFields(crm, formation) {
|
|
|
1639
1705
|
if (!formation.enabled) return [];
|
|
1640
1706
|
const def = crm.type(formation.type);
|
|
1641
1707
|
return [...formation.required, ...formation.optional].map((id10, order2) => {
|
|
1642
|
-
const
|
|
1708
|
+
const field2 = def.fields[id10];
|
|
1643
1709
|
return {
|
|
1644
1710
|
id: id10,
|
|
1645
|
-
label:
|
|
1646
|
-
type:
|
|
1711
|
+
label: field2.label ?? humanize(id10),
|
|
1712
|
+
type: field2.type,
|
|
1647
1713
|
required: formation.required.includes(id10),
|
|
1648
1714
|
order: order2,
|
|
1649
1715
|
maxLength: formation.maxLen[id10] ?? formation.defaultMaxLen,
|
|
1650
|
-
...
|
|
1651
|
-
...id10 === def.emailField ||
|
|
1716
|
+
...field2.options ? { options: [...field2.options] } : {},
|
|
1717
|
+
...id10 === def.emailField || field2.type === "email" ? { emailIdentity: "literal" } : {}
|
|
1652
1718
|
};
|
|
1653
1719
|
});
|
|
1654
1720
|
}
|
|
@@ -1684,19 +1750,19 @@ function resolveLeaderFormation(input, crm) {
|
|
|
1684
1750
|
const required = input.required ?? (type === "chapter_application" ? DEFAULT_REQUIRED2 : []);
|
|
1685
1751
|
const optional = input.optional ?? (type === "chapter_application" ? DEFAULT_OPTIONAL2 : []);
|
|
1686
1752
|
for (const [name, values] of [["required", required], ["optional", optional]]) {
|
|
1687
|
-
if (!Array.isArray(values) || !values.every((
|
|
1753
|
+
if (!Array.isArray(values) || !values.every((field2) => typeof field2 === "string" && field2 !== "")) {
|
|
1688
1754
|
throw new Error(`defineChapter.formation.${name}: must be an array of field-name strings`);
|
|
1689
1755
|
}
|
|
1690
1756
|
}
|
|
1691
|
-
const duplicates = [...required, ...optional].filter((
|
|
1757
|
+
const duplicates = [...required, ...optional].filter((field2, index2, fields) => fields.indexOf(field2) !== index2);
|
|
1692
1758
|
if (duplicates.length) throw new Error(`defineChapter.formation: duplicate public field "${duplicates[0]}"`);
|
|
1693
|
-
const unknown = [...required, ...optional].filter((
|
|
1759
|
+
const unknown = [...required, ...optional].filter((field2) => !def.fields[field2]);
|
|
1694
1760
|
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(([,
|
|
1761
|
+
const missingRequired = Object.entries(def.fields).filter(([, field2]) => field2.required).map(([field2]) => field2).filter((field2) => !required.includes(field2));
|
|
1696
1762
|
if (missingRequired.length) {
|
|
1697
1763
|
throw new Error(`defineChapter.formation.required: must include required CRM field "${missingRequired[0]}"`);
|
|
1698
1764
|
}
|
|
1699
|
-
const unsupportedRequired = required.find((
|
|
1765
|
+
const unsupportedRequired = required.find((field2) => !["string", "email", "url", "enum"].includes(def.fields[field2].type));
|
|
1700
1766
|
if (unsupportedRequired) {
|
|
1701
1767
|
throw new Error(`defineChapter.formation.required: field "${unsupportedRequired}" must be string-like`);
|
|
1702
1768
|
}
|
|
@@ -1709,9 +1775,9 @@ function resolveLeaderFormation(input, crm) {
|
|
|
1709
1775
|
if (!Number.isSafeInteger(bodyCap) || bodyCap < 1024 || bodyCap > 131072) {
|
|
1710
1776
|
throw new Error("defineChapter.formation.bodyCap: must be an integer from 1024 to 131072");
|
|
1711
1777
|
}
|
|
1712
|
-
for (const [
|
|
1713
|
-
if (![...required, ...optional].includes(
|
|
1714
|
-
throw new Error(`defineChapter.formation.maxLen.${
|
|
1778
|
+
for (const [field2, cap] of Object.entries(input.maxLen ?? {})) {
|
|
1779
|
+
if (![...required, ...optional].includes(field2) || !Number.isSafeInteger(cap) || cap < 1 || cap > 32768) {
|
|
1780
|
+
throw new Error(`defineChapter.formation.maxLen.${field2}: must name a public field and be an integer from 1 to 32768`);
|
|
1715
1781
|
}
|
|
1716
1782
|
}
|
|
1717
1783
|
return {
|
|
@@ -1801,10 +1867,10 @@ function resolveNetwork(config, crm) {
|
|
|
1801
1867
|
}
|
|
1802
1868
|
const def = crm.config.types[type];
|
|
1803
1869
|
if (!def) throw new Error(`defineChapter.network.targets.${target.id}.fields: unknown leader CRM type "${type}"`);
|
|
1804
|
-
const unknown = names.filter((
|
|
1870
|
+
const unknown = names.filter((field2) => !def.fields[field2]);
|
|
1805
1871
|
if (unknown.length > 0) {
|
|
1806
1872
|
throw new Error(
|
|
1807
|
-
`defineChapter.network.targets.${target.id}.fields.${type}: ${unknown.map((
|
|
1873
|
+
`defineChapter.network.targets.${target.id}.fields.${type}: ${unknown.map((field2) => `"${field2}"`).join(", ")} not declared on the leader CRM type`
|
|
1808
1874
|
);
|
|
1809
1875
|
}
|
|
1810
1876
|
const nameField = def.nameField ?? "name";
|
|
@@ -1892,10 +1958,10 @@ function resolveNetwork(config, crm) {
|
|
|
1892
1958
|
if (!def) {
|
|
1893
1959
|
throw new Error(`defineChapter.network.readers.${reader.id}.fields: unknown local CRM type "${type}"`);
|
|
1894
1960
|
}
|
|
1895
|
-
const unknown = names.filter((
|
|
1961
|
+
const unknown = names.filter((field2) => !def.fields[field2]);
|
|
1896
1962
|
if (unknown.length > 0) {
|
|
1897
1963
|
throw new Error(
|
|
1898
|
-
`defineChapter.network.readers.${reader.id}.fields.${type}: ${unknown.map((
|
|
1964
|
+
`defineChapter.network.readers.${reader.id}.fields.${type}: ${unknown.map((field2) => `"${field2}"`).join(", ")} not declared on the local CRM type`
|
|
1899
1965
|
);
|
|
1900
1966
|
}
|
|
1901
1967
|
const nameField = def.nameField ?? "name";
|
|
@@ -2213,6 +2279,48 @@ function defineChapter(config) {
|
|
|
2213
2279
|
return chapter;
|
|
2214
2280
|
}
|
|
2215
2281
|
|
|
2282
|
+
// src/network-commercial-seats.ts
|
|
2283
|
+
function commercialSeatItems(object2) {
|
|
2284
|
+
const items = object2.items;
|
|
2285
|
+
if (!items || items.has_more !== false || !Array.isArray(items.data) || items.data.length > 100) return void 0;
|
|
2286
|
+
const result = [];
|
|
2287
|
+
for (const item of items.data) {
|
|
2288
|
+
const metadata = item.metadata;
|
|
2289
|
+
if (!metadata?.seatId || !metadata.membershipId) continue;
|
|
2290
|
+
const price = typeof item.price === "string" ? item.price : item.price?.id;
|
|
2291
|
+
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;
|
|
2292
|
+
result.push({
|
|
2293
|
+
seatId: metadata.seatId,
|
|
2294
|
+
membershipId: metadata.membershipId,
|
|
2295
|
+
subscriptionItemId: item.id,
|
|
2296
|
+
priceId: price,
|
|
2297
|
+
...Number.isSafeInteger(item.current_period_end) ? { currentPeriodEnd: Number(item.current_period_end) } : {}
|
|
2298
|
+
});
|
|
2299
|
+
}
|
|
2300
|
+
return result;
|
|
2301
|
+
}
|
|
2302
|
+
function projectCommercialSeats(payments, subscriptions, refunds) {
|
|
2303
|
+
return payments.map((payment) => {
|
|
2304
|
+
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 })) : []);
|
|
2305
|
+
const match = matches2.length === 1 ? matches2[0] : void 0;
|
|
2306
|
+
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);
|
|
2307
|
+
return {
|
|
2308
|
+
...payment,
|
|
2309
|
+
objectKind: "seat",
|
|
2310
|
+
objectId: payment.seatId,
|
|
2311
|
+
refundedCents,
|
|
2312
|
+
...refundedCents >= Number(payment.amountReceivedCents) && refundedCents > 0 ? { status: "refunded" } : {},
|
|
2313
|
+
...match ? {
|
|
2314
|
+
subscriptionId: match.subscription.subscriptionId,
|
|
2315
|
+
subscriptionItemId: match.item.subscriptionItemId,
|
|
2316
|
+
priceId: match.item.priceId,
|
|
2317
|
+
currentPeriodEnd: match.item.currentPeriodEnd ?? match.subscription.currentPeriodEnd,
|
|
2318
|
+
cancelAtPeriodEnd: match.subscription.cancelAtPeriodEnd
|
|
2319
|
+
} : {}
|
|
2320
|
+
};
|
|
2321
|
+
});
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2216
2324
|
// src/subscription-billing-terms.ts
|
|
2217
2325
|
import { stripeRequest } from "@odla-ai/stripe";
|
|
2218
2326
|
|
|
@@ -2384,6 +2492,7 @@ function normalizeNetworkCommercialStripeEvent(input, digest) {
|
|
|
2384
2492
|
...chargeId ? { chargeId } : {},
|
|
2385
2493
|
...paymentIntentId ? { paymentIntentId } : {},
|
|
2386
2494
|
...price,
|
|
2495
|
+
...objectKind === "subscription" && commercialSeatItems(object2) ? { seatSubscriptionItems: commercialSeatItems(object2) } : {},
|
|
2387
2496
|
...status2 ? { status: status2 } : {},
|
|
2388
2497
|
...currency ? { currency } : {},
|
|
2389
2498
|
...amountCents !== void 0 ? { amountCents } : {},
|
|
@@ -2405,8 +2514,8 @@ function normalizeNetworkCommercialStripeEvent(input, digest) {
|
|
|
2405
2514
|
var identityFields = ["appId", "chapterId", "environment", "dataEnvironment", "runtime"];
|
|
2406
2515
|
function directSource(fact, sources) {
|
|
2407
2516
|
const mode = fact.livemode ? "live" : "test";
|
|
2408
|
-
const supplied = identityFields.filter((
|
|
2409
|
-
const candidates = sources.filter((source) => source.stripeMode === mode && supplied.every((
|
|
2517
|
+
const supplied = identityFields.filter((field2) => fact.identity[field2] !== void 0);
|
|
2518
|
+
const candidates = sources.filter((source) => source.stripeMode === mode && supplied.every((field2) => source[field2] === fact.identity[field2]));
|
|
2410
2519
|
if (supplied.length === identityFields.length) return candidates.length === 1 ? candidates[0] : "wrong";
|
|
2411
2520
|
if (supplied.length > 0 && candidates.length === 0) return "wrong";
|
|
2412
2521
|
return null;
|
|
@@ -2502,11 +2611,11 @@ function projectNetworkCommercialEvents(input, sources) {
|
|
|
2502
2611
|
}
|
|
2503
2612
|
}
|
|
2504
2613
|
const totalsMap = /* @__PURE__ */ new Map();
|
|
2505
|
-
const add = (row2,
|
|
2614
|
+
const add = (row2, field2, amount) => {
|
|
2506
2615
|
if (!row2.currency) return;
|
|
2507
2616
|
const key3 = `${row2.sourceKey}:${row2.currency}`;
|
|
2508
2617
|
const total = totalsMap.get(key3) ?? { sourceKey: row2.sourceKey, currency: row2.currency, paidCents: 0, refundedCents: 0, netCents: 0 };
|
|
2509
|
-
total[
|
|
2618
|
+
total[field2] += amount;
|
|
2510
2619
|
total.netCents = total.paidCents - total.refundedCents;
|
|
2511
2620
|
totalsMap.set(key3, total);
|
|
2512
2621
|
};
|
|
@@ -2514,7 +2623,7 @@ function projectNetworkCommercialEvents(input, sources) {
|
|
|
2514
2623
|
for (const row2 of seatPayments) add(row2, "paidCents", row2.amountReceivedCents ?? 0);
|
|
2515
2624
|
for (const row2 of refunds) add(row2, "refundedCents", row2.amountCents ?? 0);
|
|
2516
2625
|
return {
|
|
2517
|
-
...seatPayments.length ? { seatPayments } : {},
|
|
2626
|
+
...seatPayments.length ? { seatPayments, seats: projectCommercialSeats(seatPayments, subscriptions, refunds) } : {},
|
|
2518
2627
|
customers,
|
|
2519
2628
|
subscriptions,
|
|
2520
2629
|
invoices,
|
|
@@ -2977,7 +3086,7 @@ function findTier(tiers2, tierId) {
|
|
|
2977
3086
|
if (!tierId) return tiers2.length === 1 ? tiers2[0] : null;
|
|
2978
3087
|
return tiers2.find((tier) => tier.id === tierId) ?? null;
|
|
2979
3088
|
}
|
|
2980
|
-
function joinConfigTiers(tiers2, defaults = {}) {
|
|
3089
|
+
function joinConfigTiers(tiers2, defaults = {}, founding = /* @__PURE__ */ new Map()) {
|
|
2981
3090
|
return tiers2.map((tier) => ({
|
|
2982
3091
|
id: tier.id,
|
|
2983
3092
|
name: tier.name,
|
|
@@ -2985,7 +3094,8 @@ function joinConfigTiers(tiers2, defaults = {}) {
|
|
|
2985
3094
|
currency: tier.currency ?? defaults.currency ?? "usd",
|
|
2986
3095
|
interval: tier.interval ?? defaults.interval ?? "year",
|
|
2987
3096
|
blurb: tier.blurb ?? "",
|
|
2988
|
-
free: tierIsFree(tier)
|
|
3097
|
+
free: tierIsFree(tier),
|
|
3098
|
+
...founding.has(tier.id) ? { founding: founding.get(tier.id) } : {}
|
|
2989
3099
|
}));
|
|
2990
3100
|
}
|
|
2991
3101
|
|
|
@@ -3088,8 +3198,8 @@ function createChapterIntegration(chapter, options = {}) {
|
|
|
3088
3198
|
}
|
|
3089
3199
|
|
|
3090
3200
|
// src/membership-policy.ts
|
|
3091
|
-
var fail2 = (
|
|
3092
|
-
throw new TypeError(`invalid membership policy: ${
|
|
3201
|
+
var fail2 = (field2) => {
|
|
3202
|
+
throw new TypeError(`invalid membership policy: ${field2}`);
|
|
3093
3203
|
};
|
|
3094
3204
|
var object = (value) => {
|
|
3095
3205
|
if (!value || typeof value !== "object" || Array.isArray(value)) return fail2("object");
|
|
@@ -3271,23 +3381,63 @@ async function verifyIssuedMembershipOffer(value, expected) {
|
|
|
3271
3381
|
if (!value || typeof value !== "object" || Array.isArray(value)) return reject("invalid_offer_snapshot");
|
|
3272
3382
|
return restore({ ...value, approvedFoundingCount: 0 }, expected);
|
|
3273
3383
|
}
|
|
3274
|
-
async function
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3384
|
+
async function previewMembershipCommercialOffer(db, input) {
|
|
3385
|
+
for (const key3 of ["appId", "chapterId", "environment", "runtime", "skuId"]) {
|
|
3386
|
+
if (typeof input[key3] !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_.:-]{1,159}$/.test(input[key3])) reject("invalid_offer_identity");
|
|
3387
|
+
}
|
|
3388
|
+
if (!Number.isSafeInteger(input.now) || input.now < 0) reject("invalid_offer_window");
|
|
3389
|
+
const state = await authoritativeState(db, input);
|
|
3390
|
+
const { offering, policy } = await currentPolicy(db, input, false, state);
|
|
3391
|
+
const approvedFoundingCount = policy ? state.cohortCounts[policy.cohortLabel] ?? 0 : 0;
|
|
3392
|
+
if (!Number.isSafeInteger(approvedFoundingCount) || Number(approvedFoundingCount) < 0) reject("invalid_cohort_count");
|
|
3393
|
+
const contract = createMembershipCommercialOffer({
|
|
3394
|
+
offering,
|
|
3395
|
+
discountPolicy: policy,
|
|
3396
|
+
approvedFoundingCount: Number(approvedFoundingCount),
|
|
3397
|
+
now: input.now,
|
|
3398
|
+
couponId: await couponId(input, policy?.id)
|
|
3399
|
+
});
|
|
3400
|
+
const discount = contract.billingTerms.discount;
|
|
3401
|
+
return {
|
|
3402
|
+
skuId: input.skuId,
|
|
3403
|
+
offeringVersionId: offering.id,
|
|
3404
|
+
basePriceCents: offering.basePriceCents,
|
|
3405
|
+
currency: offering.currency,
|
|
3406
|
+
interval: offering.interval,
|
|
3407
|
+
founding: contract.founding,
|
|
3408
|
+
...contract.founding && contract.discountPolicy ? { cohortLabel: contract.discountPolicy.cohortLabel } : {},
|
|
3409
|
+
dueTodayCents: contract.dueTodayCents,
|
|
3410
|
+
renewalCents: contract.renewalCents,
|
|
3411
|
+
...discount ? { discountDuration: discount.duration === "once" ? "first_term" : "while_active" } : {},
|
|
3412
|
+
...contract.initialTermEndsAt !== void 0 ? { initialTermEndsAt: contract.initialTermEndsAt } : {}
|
|
3413
|
+
};
|
|
3414
|
+
}
|
|
3415
|
+
async function authoritativeState(db, input) {
|
|
3416
|
+
const state = await load(db, "membershipAuthorityState", `${input.chapterId}:${input.environment}`);
|
|
3278
3417
|
if (!state || state.mode !== "authoritative" || state.chapterId !== input.chapterId || state.environment !== input.environment || !Number.isSafeInteger(state.revision) || Number(state.revision) < 1 || !state.cohortCounts || typeof state.cohortCounts !== "object" || Array.isArray(state.cohortCounts)) return reject("membership_authority_unavailable");
|
|
3279
|
-
|
|
3280
|
-
|
|
3418
|
+
return state;
|
|
3419
|
+
}
|
|
3420
|
+
async function currentPolicy(db, input, restartAware, state) {
|
|
3281
3421
|
const rows3 = await db.query({
|
|
3282
3422
|
membershipOfferingVersion: { $: { where: { chapterId: input.chapterId }, limit: 200 } },
|
|
3283
3423
|
discountPolicyVersion: { $: { where: { chapterId: input.chapterId }, limit: 200 } }
|
|
3284
3424
|
});
|
|
3285
3425
|
if (!Array.isArray(rows3.membershipOfferingVersion) || !Array.isArray(rows3.discountPolicyVersion) || rows3.membershipOfferingVersion.length >= 200 || rows3.discountPolicyVersion.length >= 200) return reject("policy_history_incomplete");
|
|
3286
|
-
const offerings = rows3.membershipOfferingVersion.map((
|
|
3287
|
-
const policies = rows3.discountPolicyVersion.map((
|
|
3426
|
+
const offerings = rows3.membershipOfferingVersion.map((row2) => membershipOfferingVersion(row2.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));
|
|
3427
|
+
const policies = rows3.discountPolicyVersion.map((row2) => membershipDiscountPolicyVersion(row2.snapshot)).filter((policy2) => policy2.chapterId === input.chapterId && policy2.status === "published" && policy2.cohortEligibleSkuIds.includes(input.skuId) && input.now >= policy2.qualifyingStartsAt && input.now < policy2.qualifyingEndsAt);
|
|
3288
3428
|
const offering = offerings.sort((a, b) => b.version - a.version)[0];
|
|
3289
|
-
const
|
|
3429
|
+
const restart = restartAware && input.applicationId ? await load(db, "membershipRestart", (await membershipOfferIdForRequest({ ...input, applicationId: input.applicationId, requestId: "checkout_binding" })).replace(/^offer_/, "membership_")) : void 0;
|
|
3430
|
+
const policy = restart ? void 0 : policies.sort((a, b) => b.version - a.version)[0];
|
|
3290
3431
|
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");
|
|
3432
|
+
return { offering, policy };
|
|
3433
|
+
}
|
|
3434
|
+
async function issueMembershipCommercialOffer(db, input) {
|
|
3435
|
+
validate(input);
|
|
3436
|
+
const stateId = `${input.chapterId}:${input.environment}`;
|
|
3437
|
+
const state = await authoritativeState(db, input);
|
|
3438
|
+
const existing = await load(db, "membershipCheckoutOffer", input.offerId);
|
|
3439
|
+
if (existing) return restore(existing, input);
|
|
3440
|
+
const { offering, policy } = await currentPolicy(db, input, true, state);
|
|
3291
3441
|
const approvedFoundingCount = policy ? state.cohortCounts[policy.cohortLabel] ?? 0 : 0;
|
|
3292
3442
|
if (!Number.isSafeInteger(approvedFoundingCount) || Number(approvedFoundingCount) < 0) reject("invalid_cohort_count");
|
|
3293
3443
|
const contract = createMembershipCommercialOffer({
|
|
@@ -3324,8 +3474,73 @@ async function issueMembershipCommercialOffer(db, input) {
|
|
|
3324
3474
|
return restore(stored, input);
|
|
3325
3475
|
}
|
|
3326
3476
|
|
|
3327
|
-
// src/membership-authority
|
|
3328
|
-
import {
|
|
3477
|
+
// src/membership-cancel-authority.ts
|
|
3478
|
+
import { stripeRequest as stripeRequest2 } from "@odla-ai/stripe";
|
|
3479
|
+
|
|
3480
|
+
// src/commercial-evidence-guard.ts
|
|
3481
|
+
async function commercialEvidenceGuard(db, eventId, digest, code = "membership_payment_evidence_quarantined") {
|
|
3482
|
+
const ns = "networkCommercialExceptions", id10 = `network-commercial-exception:${eventId}:collision`;
|
|
3483
|
+
const row2 = (await db.query({ [ns]: { $: { where: { id: id10 }, limit: 1 } } }))[ns]?.[0];
|
|
3484
|
+
if (!row2) return { ns, id: id10, exists: false };
|
|
3485
|
+
if (row2.status !== "resolved" || row2.resolvedDigest !== digest || typeof digest !== "string") {
|
|
3486
|
+
throw Object.assign(new Error(code), { code });
|
|
3487
|
+
}
|
|
3488
|
+
return { ns, id: id10, exists: true, equals: { status: "resolved", resolvedDigest: digest, updatedAt: row2.updatedAt } };
|
|
3489
|
+
}
|
|
3490
|
+
|
|
3491
|
+
// src/membership-authority-state.ts
|
|
3492
|
+
var membershipFail = (code) => {
|
|
3493
|
+
throw Object.assign(new Error(code), { code });
|
|
3494
|
+
};
|
|
3495
|
+
var membershipLoad = async (db, ns, id10) => (await db.query({ [ns]: { $: { where: { id: id10 }, limit: 1 } } }))[ns]?.[0];
|
|
3496
|
+
var membershipScopeKeys = ["appId", "chapterId", "environment", "runtime", "applicationId", "skuId"];
|
|
3497
|
+
var membershipScope = (input) => Object.fromEntries(membershipScopeKeys.map((key3) => [key3, input[key3]]));
|
|
3498
|
+
function requireMembershipScope(row2, input) {
|
|
3499
|
+
if (!row2 || membershipScopeKeys.some((key3) => row2[key3] !== input[key3])) membershipFail("membership_scope_mismatch");
|
|
3500
|
+
}
|
|
3501
|
+
async function loadMembershipAuthorityState(db, input) {
|
|
3502
|
+
const row2 = await membershipLoad(db, "membershipAuthorityState", `${input.chapterId}:${input.environment}`);
|
|
3503
|
+
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");
|
|
3504
|
+
return row2;
|
|
3505
|
+
}
|
|
3506
|
+
var membershipUpdate = (ns, id10, attrs) => ({ t: "update", ns, id: id10, attrs });
|
|
3507
|
+
var membershipClock = (now) => {
|
|
3508
|
+
if (!Number.isSafeInteger(now) || now < 0) membershipFail("invalid_membership_clock");
|
|
3509
|
+
};
|
|
3510
|
+
|
|
3511
|
+
// src/membership-restart-payment.ts
|
|
3512
|
+
async function membershipRestartPayment(db, target, member, now) {
|
|
3513
|
+
const id10 = String(member.id), restart = await membershipLoad(db, "membershipRestart", id10);
|
|
3514
|
+
requireMembershipScope(restart, target);
|
|
3515
|
+
const entitlement = await membershipLoad(db, "networkEntitlement", `${id10}:primary`);
|
|
3516
|
+
requireMembershipScope(entitlement, target);
|
|
3517
|
+
if (entitlement.membershipId !== id10 || entitlement.kind !== "primary") membershipFail("membership_restart_conflict");
|
|
3518
|
+
if (restart.status !== "ready" || member.status !== "restarting" || member.restartId !== restart.restartId || member.memberNumber !== restart.memberNumber || !Number.isSafeInteger(member.journeyApprovedAt)) membershipFail("membership_restart_conflict");
|
|
3519
|
+
return {
|
|
3520
|
+
restartId: String(restart.restartId),
|
|
3521
|
+
entitlementAttrs: { revision: Number(entitlement.revision) + 1, createdAt: entitlement.createdAt },
|
|
3522
|
+
attrs: {
|
|
3523
|
+
memberNumber: member.memberNumber,
|
|
3524
|
+
journeyApprovedAt: member.journeyApprovedAt,
|
|
3525
|
+
founding: member.founding,
|
|
3526
|
+
cohort: member.cohort,
|
|
3527
|
+
...member.personId ? { personId: member.personId } : {},
|
|
3528
|
+
status: "active",
|
|
3529
|
+
revision: Number(member.revision) + 1,
|
|
3530
|
+
createdAt: member.createdAt,
|
|
3531
|
+
graceEndsAt: 0
|
|
3532
|
+
},
|
|
3533
|
+
ops: [
|
|
3534
|
+
membershipUpdate("membershipRestart", id10, { status: "complete" }),
|
|
3535
|
+
{ t: "retract", ns: "networkMembership", id: id10, attrs: ["refundRequestedAt", "cancellationRequestedAt", "discountPolicyVersionId", "discountSnapshot"] }
|
|
3536
|
+
],
|
|
3537
|
+
guards: [
|
|
3538
|
+
{ ns: "networkMembership", id: id10, exists: true, equals: { status: "restarting", revision: member.revision, restartId: restart.restartId } },
|
|
3539
|
+
{ ns: "membershipRestart", id: id10, exists: true, equals: { status: "ready", restartId: restart.restartId } },
|
|
3540
|
+
{ ns: "networkEntitlement", id: String(entitlement.id), exists: true, equals: { revision: entitlement.revision, status: entitlement.status } }
|
|
3541
|
+
]
|
|
3542
|
+
};
|
|
3543
|
+
}
|
|
3329
3544
|
|
|
3330
3545
|
// src/membership-checkout-binding.ts
|
|
3331
3546
|
var keys = ["appId", "chapterId", "environment", "runtime", "applicationId", "skuId", "offerId", "offerDigest", "quoteDigest", "priceId"];
|
|
@@ -3352,10 +3567,11 @@ async function bindMembershipCheckout(db, input, now) {
|
|
|
3352
3567
|
const row2 = await load2(db, "membershipCheckoutOffer", input.offerId);
|
|
3353
3568
|
const offer = await verifyIssuedMembershipOffer(row2, { ...input, now, lifetimeMs: 6e4 });
|
|
3354
3569
|
if (offer.digest !== input.offerDigest || offer.expiresAt <= now || offer.issuedAt > now) return reject2("membership_offer_expired");
|
|
3570
|
+
const restart = await load2(db, "membershipRestart", id10.replace(/^checkout_/, "membership_"));
|
|
3355
3571
|
const receipt = { id: id10, ...Object.fromEntries(keys.map((key3) => [key3, input[key3]])), boundAt: now };
|
|
3356
3572
|
try {
|
|
3357
3573
|
await db.transact([{ t: "update", ns: "membershipOfferCheckout", id: id10, attrs: receipt }], {
|
|
3358
|
-
mutationId: `membership-checkout:${id10}`,
|
|
3574
|
+
mutationId: `membership-checkout:${id10}${restart?.restartId ? `:${restart.restartId}` : ""}`,
|
|
3359
3575
|
guards: [
|
|
3360
3576
|
{ ns: "membershipOfferCheckout", id: id10, exists: false },
|
|
3361
3577
|
{ ns: "membershipCheckoutOffer", id: input.offerId, exists: true, equals: { digest: input.offerDigest } },
|
|
@@ -3370,26 +3586,6 @@ async function bindMembershipCheckout(db, input, now) {
|
|
|
3370
3586
|
return verifyMembershipCheckoutBinding(saved, input);
|
|
3371
3587
|
}
|
|
3372
3588
|
|
|
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
3589
|
// src/membership-payment-authority.ts
|
|
3394
3590
|
async function networkMembershipId(input) {
|
|
3395
3591
|
return (await membershipCheckoutBindingId(input)).replace(/^checkout_/, "membership_");
|
|
@@ -3407,9 +3603,7 @@ async function confirmMembershipPayment(db, input, now) {
|
|
|
3407
3603
|
if (binding.offerDigest !== offer.digest) membershipFail("membership_checkout_conflict");
|
|
3408
3604
|
const eventRow = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
|
|
3409
3605
|
if (!eventRow) membershipFail("membership_payment_evidence_pending");
|
|
3410
|
-
|
|
3411
|
-
membershipFail("membership_payment_evidence_quarantined");
|
|
3412
|
-
}
|
|
3606
|
+
const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, eventRow.digest);
|
|
3413
3607
|
const fact = eventRow.fact;
|
|
3414
3608
|
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
3609
|
appId: input.appId,
|
|
@@ -3425,8 +3619,9 @@ async function confirmMembershipPayment(db, input, now) {
|
|
|
3425
3619
|
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
3620
|
const id10 = await networkMembershipId(input), entitlementId = `${id10}:primary`;
|
|
3427
3621
|
let member = await membershipLoad(db, "networkMembership", id10);
|
|
3428
|
-
if (!member) {
|
|
3622
|
+
if (!member || member.status === "restarting") {
|
|
3429
3623
|
const offering = offer.contract.offering;
|
|
3624
|
+
const restart = member ? await membershipRestartPayment(db, input, member, now) : null;
|
|
3430
3625
|
const attrs = {
|
|
3431
3626
|
id: id10,
|
|
3432
3627
|
membershipId: id10,
|
|
@@ -3458,7 +3653,8 @@ async function confirmMembershipPayment(db, input, now) {
|
|
|
3458
3653
|
refundedCents: 0,
|
|
3459
3654
|
revision: 1,
|
|
3460
3655
|
createdAt: now,
|
|
3461
|
-
updatedAt: now
|
|
3656
|
+
updatedAt: now,
|
|
3657
|
+
...restart?.attrs
|
|
3462
3658
|
};
|
|
3463
3659
|
try {
|
|
3464
3660
|
await db.transact([
|
|
@@ -3468,21 +3664,23 @@ async function confirmMembershipPayment(db, input, now) {
|
|
|
3468
3664
|
...membershipScope(input),
|
|
3469
3665
|
membershipId: id10,
|
|
3470
3666
|
kind: "primary",
|
|
3471
|
-
status: "pending_approval",
|
|
3667
|
+
status: restart?.attrs.status === "active" ? "active" : "pending_approval",
|
|
3472
3668
|
termStartedAt: paidAt,
|
|
3473
3669
|
termEndsAt: endsAt,
|
|
3474
3670
|
offeringSnapshot: offering,
|
|
3475
3671
|
sourceEventId: input.eventId,
|
|
3476
3672
|
revision: 1,
|
|
3477
3673
|
createdAt: now,
|
|
3478
|
-
updatedAt: now
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3674
|
+
updatedAt: now,
|
|
3675
|
+
...restart?.entitlementAttrs
|
|
3676
|
+
}),
|
|
3677
|
+
...restart?.ops ?? []
|
|
3678
|
+
], { mutationId: `membership-paid:${id10}${restart ? `:${restart.restartId}` : ""}`, guards: [
|
|
3679
|
+
...restart?.guards ?? [{ ns: "networkMembership", id: id10, exists: false }],
|
|
3482
3680
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
|
|
3483
3681
|
{ ns: "membershipOfferCheckout", id: String(binding.id), exists: true, equals: { offerDigest: offer.digest } },
|
|
3484
3682
|
{ ns: "networkCommercialEvents", id: String(eventRow.id), exists: true, equals: { digest: eventRow.digest } },
|
|
3485
|
-
|
|
3683
|
+
evidenceGuard
|
|
3486
3684
|
] });
|
|
3487
3685
|
} catch (error) {
|
|
3488
3686
|
if (error.code !== "transact_guard_failed") throw error;
|
|
@@ -3501,6 +3699,57 @@ async function confirmMembershipPayment(db, input, now) {
|
|
|
3501
3699
|
};
|
|
3502
3700
|
}
|
|
3503
3701
|
|
|
3702
|
+
// src/membership-cancel-authority.ts
|
|
3703
|
+
async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
|
|
3704
|
+
membershipClock(now);
|
|
3705
|
+
const id10 = await networkMembershipId(target);
|
|
3706
|
+
const state = await loadMembershipAuthorityState(db, target);
|
|
3707
|
+
const member = await membershipLoad(db, "networkMembership", id10);
|
|
3708
|
+
requireMembershipScope(member, target);
|
|
3709
|
+
if (!["active", "cancel_at_term_end", "payment_grace", "paid_pending_approval", "lapsed"].includes(String(member.status))) {
|
|
3710
|
+
membershipFail("membership_cancellation_unavailable");
|
|
3711
|
+
}
|
|
3712
|
+
const path = `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`;
|
|
3713
|
+
const read2 = await stripeRequest2({ secretKey: stripeSecret }, "GET", path);
|
|
3714
|
+
const sub = read2.body;
|
|
3715
|
+
if (!read2.ok || sub.id !== member.providerSubscriptionId || sub.customer !== member.providerCustomerId || sub.livemode !== (target.environment === "prod") || !Object.entries({
|
|
3716
|
+
...stripePartitionMetadata(target),
|
|
3717
|
+
applicationId: target.applicationId
|
|
3718
|
+
}).every(([key3, value]) => sub.metadata?.[key3] === value)) membershipFail("membership_cancellation_provider_mismatch");
|
|
3719
|
+
if (sub.status !== "canceled" && sub.cancel_at_period_end !== true) {
|
|
3720
|
+
const write = await stripeRequest2(
|
|
3721
|
+
{ secretKey: stripeSecret },
|
|
3722
|
+
"POST",
|
|
3723
|
+
path,
|
|
3724
|
+
{ cancel_at_period_end: true, proration_behavior: "none" },
|
|
3725
|
+
{ idempotencyKey: `membership-cancel-at-term:${id10}:${member.providerSubscriptionId}` }
|
|
3726
|
+
);
|
|
3727
|
+
if (!write.ok || write.body.id !== member.providerSubscriptionId || write.body.cancel_at_period_end !== true) {
|
|
3728
|
+
membershipFail("membership_cancellation_pending");
|
|
3729
|
+
}
|
|
3730
|
+
}
|
|
3731
|
+
const status2 = Number(member.termEndsAt) <= now ? "lapsed" : member.journeyApprovedAt ? "cancel_at_term_end" : "paid_pending_approval";
|
|
3732
|
+
if (member.status !== status2 || member.autoRenew !== false || Number(member.graceEndsAt ?? 0) !== 0) {
|
|
3733
|
+
await db.transact([membershipUpdate("networkMembership", id10, {
|
|
3734
|
+
status: status2,
|
|
3735
|
+
autoRenew: false,
|
|
3736
|
+
graceEndsAt: 0,
|
|
3737
|
+
cancellationRequestedAt: now,
|
|
3738
|
+
revision: Number(member.revision) + 1,
|
|
3739
|
+
updatedAt: now
|
|
3740
|
+
})], {
|
|
3741
|
+
guards: [
|
|
3742
|
+
{ ns: "networkMembership", id: id10, exists: true, equals: { revision: member.revision } },
|
|
3743
|
+
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { revision: state.revision, mode: "authoritative" } }
|
|
3744
|
+
]
|
|
3745
|
+
});
|
|
3746
|
+
}
|
|
3747
|
+
return { membershipId: id10, status: status2, termEndsAt: Number(member.termEndsAt), graceEndsAt: 0, autoRenew: false };
|
|
3748
|
+
}
|
|
3749
|
+
|
|
3750
|
+
// src/membership-authority-handler.ts
|
|
3751
|
+
import { FEDERATION_HEADERS, verifyFederatedRequest } from "@odla-ai/db";
|
|
3752
|
+
|
|
3504
3753
|
// src/membership-seat-activation.ts
|
|
3505
3754
|
async function namedSeatPrimaryActivation(db, member, now) {
|
|
3506
3755
|
if (!member.namedSeatId) return { ops: [], guards: [] };
|
|
@@ -3593,7 +3842,7 @@ async function approveNetworkMembership(db, input, now) {
|
|
|
3593
3842
|
}
|
|
3594
3843
|
|
|
3595
3844
|
// src/payments-refund.ts
|
|
3596
|
-
import { stripeRequest as
|
|
3845
|
+
import { stripeRequest as stripeRequest3 } from "@odla-ai/stripe";
|
|
3597
3846
|
var row = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
3598
3847
|
var id7 = (value) => typeof value === "string" ? value : String(row(value).id ?? "");
|
|
3599
3848
|
var providerId2 = (value, prefix) => new RegExp(`^${prefix}_[A-Za-z0-9]+$`).test(value);
|
|
@@ -3603,7 +3852,7 @@ function fail3(code = "refund_target_mismatch") {
|
|
|
3603
3852
|
async function refundSubscriptionPayment(secretKey, input) {
|
|
3604
3853
|
if (!providerId2(input.customerId, "cus") || !providerId2(input.subscriptionId, "sub")) fail3();
|
|
3605
3854
|
const call = async (method, path, params, key3) => {
|
|
3606
|
-
const result = await
|
|
3855
|
+
const result = await stripeRequest3({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
|
|
3607
3856
|
if (!result.ok) fail3("stripe_error");
|
|
3608
3857
|
if (!result.body || typeof result.body !== "object" || Array.isArray(result.body)) fail3("stripe_invalid_response");
|
|
3609
3858
|
return result.body;
|
|
@@ -3774,12 +4023,13 @@ async function refundNetworkMembership(db, input, secretKey, now) {
|
|
|
3774
4023
|
}
|
|
3775
4024
|
|
|
3776
4025
|
// src/membership-provider-lifecycle.ts
|
|
3777
|
-
import { stripeRequest as
|
|
4026
|
+
import { stripeRequest as stripeRequest4 } from "@odla-ai/stripe";
|
|
3778
4027
|
|
|
3779
4028
|
// src/membership-renewal-authority.ts
|
|
3780
4029
|
async function reconcileMembershipRenewal(db, input, now) {
|
|
3781
4030
|
membershipClock(now);
|
|
3782
4031
|
const id10 = await networkMembershipId(input), eventRow = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
|
|
4032
|
+
const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, eventRow?.digest, "membership_renewal_evidence_pending");
|
|
3783
4033
|
const fact = eventRow?.fact;
|
|
3784
4034
|
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
4035
|
appId: input.appId,
|
|
@@ -3787,7 +4037,7 @@ async function reconcileMembershipRenewal(db, input, now) {
|
|
|
3787
4037
|
environment: input.environment,
|
|
3788
4038
|
dataEnvironment: input.environment,
|
|
3789
4039
|
runtime: input.runtime
|
|
3790
|
-
}).some(([key3, value]) => fact.identity[key3] !== value)
|
|
4040
|
+
}).some(([key3, value]) => fact.identity[key3] !== value)) {
|
|
3791
4041
|
return membershipFail("membership_renewal_evidence_pending");
|
|
3792
4042
|
}
|
|
3793
4043
|
for (let attempt = 0; attempt < 8; attempt++) {
|
|
@@ -3801,7 +4051,7 @@ async function reconcileMembershipRenewal(db, input, now) {
|
|
|
3801
4051
|
autoRenew: member.autoRenew === true
|
|
3802
4052
|
});
|
|
3803
4053
|
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();
|
|
4054
|
+
if (["refunded", "refund_pending", "canceled", "declined", "restarting", "superseded"].includes(String(member.status))) return receipt();
|
|
3805
4055
|
const lines = fact.recurringInvoiceLines;
|
|
3806
4056
|
if (!lines?.length || lines.length > 2) membershipFail("membership_renewal_lines_missing");
|
|
3807
4057
|
const start = lines[0].startsAt, end = lines[0].endsAt;
|
|
@@ -3810,7 +4060,7 @@ async function reconcileMembershipRenewal(db, input, now) {
|
|
|
3810
4060
|
if (start !== member.termEndsAt || start > now + 3e4) membershipFail("membership_renewal_period_gap");
|
|
3811
4061
|
const seat = member.namedSeatId ? await membershipLoad(db, "networkSeat", String(member.namedSeatId)) : void 0;
|
|
3812
4062
|
if (seat) requireMembershipScope(seat, input);
|
|
3813
|
-
const billedSeat = seat && seat.providerSubscriptionItemId && !["refunded", "refund_pending"].includes(String(seat.status)) ? seat : void 0;
|
|
4063
|
+
const billedSeat = seat && seat.providerSubscriptionId === member.providerSubscriptionId && seat.providerSubscriptionItemId && !["refunded", "refund_pending"].includes(String(seat.status)) ? seat : void 0;
|
|
3814
4064
|
const prices = [String(member.providerPriceId), ...billedSeat ? [String(billedSeat.priceId)] : []].sort();
|
|
3815
4065
|
const amount = Number(member.renewalAmountCents) + Number(billedSeat?.amountCents ?? 0);
|
|
3816
4066
|
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 +4093,7 @@ async function reconcileMembershipRenewal(db, input, now) {
|
|
|
3843
4093
|
{ ns: "networkEntitlement", id: `${id10}:primary`, exists: true, equals: { revision: entitlement.revision } },
|
|
3844
4094
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { revision: state.revision, mode: "authoritative" } },
|
|
3845
4095
|
{ ns: "networkCommercialEvents", id: String(eventRow.id), exists: true, equals: { digest: eventRow.digest } },
|
|
3846
|
-
|
|
4096
|
+
evidenceGuard
|
|
3847
4097
|
];
|
|
3848
4098
|
if (seat) guards.push({ ns: "networkSeat", id: String(seat.id), exists: true, equals: { revision: seat.revision } });
|
|
3849
4099
|
if (billedSeat?.entitlementId) {
|
|
@@ -3889,14 +4139,14 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
3889
4139
|
const row2 = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
|
|
3890
4140
|
const fact = row2?.fact;
|
|
3891
4141
|
if (fact?.objectKind === "invoice") return reconcileMembershipRenewal(db, input, now);
|
|
3892
|
-
const
|
|
4142
|
+
const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, row2?.digest, "membership_billing_evidence_pending");
|
|
3893
4143
|
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
4144
|
appId: input.appId,
|
|
3895
4145
|
chapterId: input.chapterId,
|
|
3896
4146
|
environment: input.environment,
|
|
3897
4147
|
dataEnvironment: input.environment,
|
|
3898
4148
|
runtime: input.runtime
|
|
3899
|
-
}).some(([key3, value]) => fact.identity[key3] !== value)
|
|
4149
|
+
}).some(([key3, value]) => fact.identity[key3] !== value)) membershipFail("membership_billing_evidence_pending");
|
|
3900
4150
|
const sk = await getVaultSecret(db, stripeSecretName);
|
|
3901
4151
|
if (!sk) membershipFail("membership_billing_provider_unavailable");
|
|
3902
4152
|
const id10 = await networkMembershipId(input);
|
|
@@ -3910,6 +4160,7 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
3910
4160
|
graceEndsAt: Number(value.graceEndsAt ?? 0),
|
|
3911
4161
|
autoRenew: value.autoRenew === true
|
|
3912
4162
|
});
|
|
4163
|
+
if (["restarting"].includes(String(member.status))) return receipt();
|
|
3913
4164
|
if (fact.customerId !== member.providerCustomerId) membershipFail("membership_billing_scope_mismatch");
|
|
3914
4165
|
if (fact.eventType === "charge.refunded") {
|
|
3915
4166
|
if (!fact.chargeId || fact.seatId || fact.amountCents !== member.amountCents || fact.currency !== member.currency) membershipFail("membership_refund_evidence_mismatch");
|
|
@@ -3922,15 +4173,15 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
3922
4173
|
alreadyRefundedChargeId: fact.chargeId,
|
|
3923
4174
|
cancelSubscription: false
|
|
3924
4175
|
});
|
|
3925
|
-
|
|
4176
|
+
await commercialEvidenceGuard(db, input.eventId, row2?.digest, "membership_billing_evidence_pending");
|
|
3926
4177
|
await refundNetworkMembership(db, input, sk, now);
|
|
3927
4178
|
const saved2 = await membershipLoad(db, "networkMembership", id10);
|
|
3928
4179
|
requireMembershipScope(saved2, input);
|
|
3929
4180
|
return receipt(saved2);
|
|
3930
4181
|
}
|
|
3931
4182
|
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
|
|
4183
|
+
if (["refund_pending", "refunded", "declined", "canceled", "restarting"].includes(String(member.status))) return receipt();
|
|
4184
|
+
const result = await stripeRequest4({ secretKey: sk }, "GET", `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`);
|
|
3934
4185
|
const sub = result.body;
|
|
3935
4186
|
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
4187
|
membershipFail("membership_billing_provider_mismatch");
|
|
@@ -3954,7 +4205,7 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
3954
4205
|
{ ns: "networkEntitlement", id: String(entitlement.id), exists: true, equals: { revision: entitlement.revision } },
|
|
3955
4206
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
|
|
3956
4207
|
{ ns: "networkCommercialEvents", id: String(row2.id), exists: true, equals: { digest: fact.digest } },
|
|
3957
|
-
|
|
4208
|
+
evidenceGuard
|
|
3958
4209
|
] });
|
|
3959
4210
|
} catch (error) {
|
|
3960
4211
|
if (error.code !== "transact_guard_failed") throw error;
|
|
@@ -3967,6 +4218,120 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
3967
4218
|
return membershipFail("membership_billing_retry_required");
|
|
3968
4219
|
}
|
|
3969
4220
|
|
|
4221
|
+
// src/membership-restart-seat.ts
|
|
4222
|
+
async function expireRestartedMembershipSeat(db, target, member, now) {
|
|
4223
|
+
const ops = [], guards = [];
|
|
4224
|
+
if (!member.namedSeatId) return { ops, guards };
|
|
4225
|
+
const seat = await membershipLoad(db, "networkSeat", String(member.namedSeatId));
|
|
4226
|
+
requireMembershipScope(seat, target);
|
|
4227
|
+
if (!["refunded", "refund_pending", "expired"].includes(String(seat.status))) {
|
|
4228
|
+
ops.push(membershipUpdate("networkSeat", String(seat.id), { status: "expired", revision: Number(seat.revision) + 1, updatedAt: now }));
|
|
4229
|
+
guards.push({ ns: "networkSeat", id: String(seat.id), exists: true, equals: { revision: seat.revision } });
|
|
4230
|
+
}
|
|
4231
|
+
if (seat.entitlementId) {
|
|
4232
|
+
const entitlement = await membershipLoad(db, "networkEntitlement", String(seat.entitlementId));
|
|
4233
|
+
requireMembershipScope(entitlement, target);
|
|
4234
|
+
ops.push(membershipUpdate("networkEntitlement", String(entitlement.id), { status: "closed", revision: Number(entitlement.revision) + 1, updatedAt: now }));
|
|
4235
|
+
guards.push({ ns: "networkEntitlement", id: String(entitlement.id), exists: true, equals: { revision: entitlement.revision } });
|
|
4236
|
+
}
|
|
4237
|
+
return { ops, guards };
|
|
4238
|
+
}
|
|
4239
|
+
|
|
4240
|
+
// src/membership-restart-authority.ts
|
|
4241
|
+
import { stripeRequest as stripeRequest5 } from "@odla-ai/stripe";
|
|
4242
|
+
|
|
4243
|
+
// src/stripe-event-digest.ts
|
|
4244
|
+
async function stripePayloadDigest(value) {
|
|
4245
|
+
const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
|
|
4246
|
+
return [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
4247
|
+
}
|
|
4248
|
+
|
|
4249
|
+
// src/membership-restart-authority.ts
|
|
4250
|
+
function membershipCanRestart(member, now) {
|
|
4251
|
+
const end = member.status === "payment_grace" ? Number(member.graceEndsAt) : Number(member.termEndsAt);
|
|
4252
|
+
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);
|
|
4253
|
+
}
|
|
4254
|
+
async function stopPreviousSubscription(member, target, secret) {
|
|
4255
|
+
const path = `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`;
|
|
4256
|
+
const result = await stripeRequest5({ secretKey: secret }, "GET", path);
|
|
4257
|
+
const sub = result.body;
|
|
4258
|
+
if (!result.ok || sub.id !== member.providerSubscriptionId || sub.customer !== member.providerCustomerId || sub.livemode !== (target.environment === "prod") || !Object.entries({
|
|
4259
|
+
...stripePartitionMetadata(target),
|
|
4260
|
+
applicationId: target.applicationId
|
|
4261
|
+
}).every(([key3, value]) => sub.metadata?.[key3] === value)) membershipFail("membership_restart_provider_mismatch");
|
|
4262
|
+
if (sub.status === "canceled") return;
|
|
4263
|
+
const canceled = await stripeRequest5(
|
|
4264
|
+
{ secretKey: secret },
|
|
4265
|
+
"DELETE",
|
|
4266
|
+
path,
|
|
4267
|
+
{ invoice_now: false, prorate: false },
|
|
4268
|
+
{ idempotencyKey: `membership-restart-cancel:${member.providerSubscriptionId}` }
|
|
4269
|
+
);
|
|
4270
|
+
if (!canceled.ok || canceled.body.id !== member.providerSubscriptionId || canceled.body.status !== "canceled") membershipFail("membership_restart_cancellation_pending");
|
|
4271
|
+
}
|
|
4272
|
+
async function prepareMembershipRestart(db, input, now, stripeSecret) {
|
|
4273
|
+
membershipClock(now);
|
|
4274
|
+
if (!/^sub_[A-Za-z0-9_]+$/.test(input.previousSubscriptionId)) membershipFail("membership_restart_conflict");
|
|
4275
|
+
const id10 = await networkMembershipId(input), bindingId = await membershipCheckoutBindingId(input);
|
|
4276
|
+
const restartId = `restart_${await stripePayloadDigest(JSON.stringify([id10, input.previousSubscriptionId]))}`;
|
|
4277
|
+
const state = await loadMembershipAuthorityState(db, input);
|
|
4278
|
+
const previous = await membershipLoad(db, "networkMembership", id10);
|
|
4279
|
+
requireMembershipScope(previous, input);
|
|
4280
|
+
const receipt = {
|
|
4281
|
+
applicationId: input.applicationId,
|
|
4282
|
+
membershipId: id10,
|
|
4283
|
+
restartId,
|
|
4284
|
+
memberNumber: Number(previous.memberNumber),
|
|
4285
|
+
status: "ready"
|
|
4286
|
+
};
|
|
4287
|
+
const existing = await membershipLoad(db, "membershipRestart", id10);
|
|
4288
|
+
if (existing?.restartId === restartId && existing.status === "ready") {
|
|
4289
|
+
requireMembershipScope(existing, input);
|
|
4290
|
+
return receipt;
|
|
4291
|
+
}
|
|
4292
|
+
if (previous.providerSubscriptionId !== input.previousSubscriptionId) membershipFail("membership_restart_conflict");
|
|
4293
|
+
if (previous.status !== "restarting") {
|
|
4294
|
+
if (!membershipCanRestart(previous, now)) membershipFail("membership_restart_not_eligible");
|
|
4295
|
+
const binding2 = await membershipLoad(db, "membershipOfferCheckout", bindingId);
|
|
4296
|
+
requireMembershipScope(binding2, input);
|
|
4297
|
+
const seats = await expireRestartedMembershipSeat(db, input, previous, now);
|
|
4298
|
+
await db.transact([
|
|
4299
|
+
...seats.ops,
|
|
4300
|
+
membershipUpdate("membershipPurchaseHistory", restartId, {
|
|
4301
|
+
id: restartId,
|
|
4302
|
+
...membershipScope(input),
|
|
4303
|
+
membershipId: id10,
|
|
4304
|
+
providerSubscriptionId: input.previousSubscriptionId,
|
|
4305
|
+
memberSnapshot: previous,
|
|
4306
|
+
bindingSnapshot: binding2,
|
|
4307
|
+
archivedAt: now
|
|
4308
|
+
}),
|
|
4309
|
+
membershipUpdate("networkMembership", id10, { status: "restarting", restartId, revision: Number(previous.revision) + 1, updatedAt: now })
|
|
4310
|
+
], { mutationId: `membership-restart-reserve:${restartId}`, guards: [
|
|
4311
|
+
...seats.guards,
|
|
4312
|
+
{ ns: "networkMembership", id: id10, exists: true, equals: { revision: previous.revision, status: previous.status } },
|
|
4313
|
+
{ ns: "membershipPurchaseHistory", id: restartId, exists: false },
|
|
4314
|
+
{ ns: "membershipOfferCheckout", id: bindingId, exists: true, equals: { offerDigest: binding2.offerDigest } },
|
|
4315
|
+
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
|
|
4316
|
+
] });
|
|
4317
|
+
} else if (previous.restartId !== restartId) membershipFail("membership_restart_conflict");
|
|
4318
|
+
const reserved = await membershipLoad(db, "networkMembership", id10);
|
|
4319
|
+
requireMembershipScope(reserved, input);
|
|
4320
|
+
if (reserved.status !== "restarting" || reserved.restartId !== restartId) membershipFail("membership_restart_conflict");
|
|
4321
|
+
await stopPreviousSubscription(reserved, input, stripeSecret);
|
|
4322
|
+
const binding = await membershipLoad(db, "membershipOfferCheckout", bindingId);
|
|
4323
|
+
requireMembershipScope(binding, input);
|
|
4324
|
+
await db.transact([
|
|
4325
|
+
membershipUpdate("membershipRestart", id10, { id: id10, ...membershipScope(input), ...receipt, previousSubscriptionId: input.previousSubscriptionId, createdAt: now }),
|
|
4326
|
+
{ t: "delete", ns: "membershipOfferCheckout", id: bindingId }
|
|
4327
|
+
], { mutationId: `membership-restart-ready:${restartId}`, guards: [
|
|
4328
|
+
{ ns: "membershipRestart", id: id10, exists: Boolean(existing), ...existing ? { equals: { restartId: existing.restartId, status: existing.status } } : {} },
|
|
4329
|
+
{ ns: "networkMembership", id: id10, exists: true, equals: { revision: reserved.revision, status: "restarting", restartId } },
|
|
4330
|
+
{ ns: "membershipOfferCheckout", id: bindingId, exists: true, equals: { offerDigest: binding.offerDigest } }
|
|
4331
|
+
] });
|
|
4332
|
+
return receipt;
|
|
4333
|
+
}
|
|
4334
|
+
|
|
3970
4335
|
// src/membership-access-authority.ts
|
|
3971
4336
|
async function readMembershipAccess(db, input, now) {
|
|
3972
4337
|
membershipClock(now);
|
|
@@ -3983,7 +4348,8 @@ async function readMembershipAccess(db, input, now) {
|
|
|
3983
4348
|
allowed: allowed2,
|
|
3984
4349
|
status: String(member.status),
|
|
3985
4350
|
accessEndsAt: Number.isSafeInteger(endsAt) ? endsAt : 0,
|
|
3986
|
-
revision: Number(member.revision)
|
|
4351
|
+
revision: Number(member.revision),
|
|
4352
|
+
restartEligible: !allowed2 && membershipCanRestart(member, now)
|
|
3987
4353
|
};
|
|
3988
4354
|
}
|
|
3989
4355
|
async function verifyMembershipAccessReceipt(value, input, now) {
|
|
@@ -3991,7 +4357,7 @@ async function verifyMembershipAccessReceipt(value, input, now) {
|
|
|
3991
4357
|
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
4358
|
throw new Error("invalid network access receipt");
|
|
3993
4359
|
}
|
|
3994
|
-
return { membershipId: row2.membershipId, allowed: row2.allowed, status: row2.status, accessEndsAt: row2.accessEndsAt, revision: row2.revision };
|
|
4360
|
+
return { membershipId: row2.membershipId, allowed: row2.allowed, status: row2.status, accessEndsAt: row2.accessEndsAt, revision: row2.revision, ...row2.restartEligible === true && !row2.allowed ? { restartEligible: true } : {} };
|
|
3995
4361
|
}
|
|
3996
4362
|
|
|
3997
4363
|
// src/membership-seat-reservation.ts
|
|
@@ -4072,16 +4438,16 @@ async function reserveNamedMembershipSeat(db, input, now) {
|
|
|
4072
4438
|
}
|
|
4073
4439
|
|
|
4074
4440
|
// src/membership-seat-stripe.ts
|
|
4075
|
-
import { stripeRequest as
|
|
4441
|
+
import { stripeRequest as stripeRequest7 } from "@odla-ai/stripe";
|
|
4076
4442
|
|
|
4077
4443
|
// src/subscription-provider-contract.ts
|
|
4078
|
-
import { stripeRequest as
|
|
4444
|
+
import { stripeRequest as stripeRequest6 } from "@odla-ai/stripe";
|
|
4079
4445
|
function subscriptionContractMismatch() {
|
|
4080
4446
|
throw Object.assign(new Error("Stripe subscription does not match the checkout contract"), { code: "stripe_contract_mismatch" });
|
|
4081
4447
|
}
|
|
4082
4448
|
async function verifySubscriptionPrice(secretKey, input, resume = false) {
|
|
4083
4449
|
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
|
|
4450
|
+
const result = await stripeRequest6({ secretKey }, "GET", `/v1/prices/${encodeURIComponent(input.priceId)}`);
|
|
4085
4451
|
const price = result.body;
|
|
4086
4452
|
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
4453
|
return typeof price.product === "string" ? price.product : void 0;
|
|
@@ -4099,7 +4465,7 @@ var metadataFor = (seat) => ({
|
|
|
4099
4465
|
});
|
|
4100
4466
|
var matches = (value, expected) => value && Object.entries(expected).every(([key3, item]) => value[key3] === item);
|
|
4101
4467
|
async function read(sk, path, params) {
|
|
4102
|
-
const response = await
|
|
4468
|
+
const response = await stripeRequest7({ secretKey: sk }, "GET", path, params);
|
|
4103
4469
|
if (!response.ok) return reject3();
|
|
4104
4470
|
return response.body;
|
|
4105
4471
|
}
|
|
@@ -4136,11 +4502,11 @@ async function createNamedSeatStripePayment(sk, seat, now) {
|
|
|
4136
4502
|
}
|
|
4137
4503
|
await subscriptionForSeat(sk, seat);
|
|
4138
4504
|
const metadata = metadataFor(seat);
|
|
4139
|
-
const result = seat.providerPaymentIntentId ? await
|
|
4505
|
+
const result = seat.providerPaymentIntentId ? await stripeRequest7(
|
|
4140
4506
|
{ secretKey: sk },
|
|
4141
4507
|
"GET",
|
|
4142
4508
|
`/v1/payment_intents/${encodeURIComponent(seat.providerPaymentIntentId)}`
|
|
4143
|
-
) : await
|
|
4509
|
+
) : await stripeRequest7({ secretKey: sk }, "POST", "/v1/payment_intents", {
|
|
4144
4510
|
amount: seat.amountCents,
|
|
4145
4511
|
currency: seat.currency,
|
|
4146
4512
|
customer: seat.providerCustomerId,
|
|
@@ -4158,7 +4524,7 @@ async function attachNamedSeatStripeRenewal(sk, seat) {
|
|
|
4158
4524
|
const sub = await subscriptionForSeat(sk, seat);
|
|
4159
4525
|
const existing = sub.items.data.find((item2) => item2.metadata?.seatId === seat.id);
|
|
4160
4526
|
if (existing) return String(existing.id);
|
|
4161
|
-
const result = await
|
|
4527
|
+
const result = await stripeRequest7({ secretKey: sk }, "POST", "/v1/subscription_items", {
|
|
4162
4528
|
subscription: seat.providerSubscriptionId,
|
|
4163
4529
|
price: seat.priceId,
|
|
4164
4530
|
quantity: 1,
|
|
@@ -4236,8 +4602,7 @@ async function confirmNamedMembershipSeatPayment(db, input, now) {
|
|
|
4236
4602
|
requireMembershipScope(member, input);
|
|
4237
4603
|
const row2 = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
|
|
4238
4604
|
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");
|
|
4605
|
+
const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, row2.digest, "named_seat_payment_evidence_quarantined");
|
|
4241
4606
|
const fact = row2.fact;
|
|
4242
4607
|
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
4608
|
appId: input.appId,
|
|
@@ -4259,7 +4624,7 @@ async function confirmNamedMembershipSeatPayment(db, input, now) {
|
|
|
4259
4624
|
membershipUpdate("networkSeat", input.seatId, {
|
|
4260
4625
|
status: status2,
|
|
4261
4626
|
purchasedAt: paidAt,
|
|
4262
|
-
acceptanceEndsAt:
|
|
4627
|
+
acceptanceEndsAt: Number(member.termEndsAt),
|
|
4263
4628
|
termStartedAt: paidAt,
|
|
4264
4629
|
termEndsAt: member.termEndsAt,
|
|
4265
4630
|
paymentEventId: input.eventId,
|
|
@@ -4284,7 +4649,7 @@ async function confirmNamedMembershipSeatPayment(db, input, now) {
|
|
|
4284
4649
|
{ ns: "networkMembership", id: String(member.id), exists: true, equals: { revision: member.revision } },
|
|
4285
4650
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
|
|
4286
4651
|
{ ns: "networkCommercialEvents", id: String(row2.id), exists: true, equals: { digest: fact.digest } },
|
|
4287
|
-
|
|
4652
|
+
evidenceGuard
|
|
4288
4653
|
] });
|
|
4289
4654
|
} catch (error) {
|
|
4290
4655
|
if (error.code !== "transact_guard_failed") throw error;
|
|
@@ -4307,7 +4672,7 @@ async function completeNamedSeatRenewal(db, seatId, secretKey, now) {
|
|
|
4307
4672
|
if (seat.status !== "renewal_pending" || !seat.purchasedAt) membershipFail("named_seat_renewal_not_ready");
|
|
4308
4673
|
const member = await membershipLoad(db, "networkMembership", target.primaryMembershipId);
|
|
4309
4674
|
requireMembershipScope(member, target);
|
|
4310
|
-
if (now >= Number(
|
|
4675
|
+
if (now >= Number(member.termEndsAt) || Number(member.refundedCents) !== 0 || !["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status))) membershipFail("named_seat_refund_required");
|
|
4311
4676
|
const itemId = await attachNamedSeatStripeRenewal(secretKey, target);
|
|
4312
4677
|
const entitlementId = `${seatId}:entitlement`, effectId = `${seatId}:attach-renewal`;
|
|
4313
4678
|
try {
|
|
@@ -4382,7 +4747,7 @@ async function recordRecipient(db, input, now, approve) {
|
|
|
4382
4747
|
requireMembershipScope(entitlement, input);
|
|
4383
4748
|
if (!["pending_acceptance", "pending_approval", "pending_primary", "active"].includes(String(seat.status)) || !["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) || Number(member.refundedCents) !== 0 || now >= Number(member.termEndsAt) || !seat.providerPaymentIntentId || !seat.providerSubscriptionItemId) membershipFail("named_seat_acceptance_unavailable");
|
|
4384
4749
|
if (seat.acceptedAt === void 0) {
|
|
4385
|
-
if (approve ||
|
|
4750
|
+
if (approve || normalizeChapterEmail(input.verifiedRecipientEmail) !== seat.recipientEmailNormalized) {
|
|
4386
4751
|
membershipFail("named_seat_recipient_verification_required");
|
|
4387
4752
|
}
|
|
4388
4753
|
} else if (seat.recipientUserId !== input.recipientUserId || seat.recipientApplicationId !== input.recipientApplicationId) {
|
|
@@ -4433,14 +4798,14 @@ async function recordRecipient(db, input, now, approve) {
|
|
|
4433
4798
|
}
|
|
4434
4799
|
|
|
4435
4800
|
// src/membership-seat-refund-stripe.ts
|
|
4436
|
-
import { stripeRequest as
|
|
4801
|
+
import { stripeRequest as stripeRequest8 } from "@odla-ai/stripe";
|
|
4437
4802
|
var fail4 = (code = "named_seat_refund_mismatch") => {
|
|
4438
4803
|
throw Object.assign(new Error(code), { code });
|
|
4439
4804
|
};
|
|
4440
4805
|
async function refundNamedSeatStripePayment(secretKey, seat) {
|
|
4441
4806
|
if (!seat.providerPaymentIntentId || seat.status !== "refund_pending") fail4();
|
|
4442
4807
|
const call = async (method, path, params, idempotencyKey) => {
|
|
4443
|
-
const result = await
|
|
4808
|
+
const result = await stripeRequest8({ secretKey }, method, path, params, idempotencyKey ? { idempotencyKey } : {});
|
|
4444
4809
|
if (!result.ok || !result.body) return fail4("named_seat_refund_provider_unavailable");
|
|
4445
4810
|
return result.body;
|
|
4446
4811
|
};
|
|
@@ -4495,7 +4860,7 @@ async function requestNamedSeatRefund(db, input, now) {
|
|
|
4495
4860
|
const member = await membershipLoad(db, "networkMembership", String(seat.primaryMembershipId));
|
|
4496
4861
|
requireMembershipScope(member, input);
|
|
4497
4862
|
if (seat.status === "refunded" || seat.status === "refund_pending") return;
|
|
4498
|
-
if (!seat.purchasedAt || !seat.providerPaymentIntentId || input.reason === "declined" && seat.journeyApprovedAt !== void 0 || input.reason === "
|
|
4863
|
+
if (!seat.purchasedAt || !seat.providerPaymentIntentId || input.reason === "declined" && seat.journeyApprovedAt !== void 0 || input.reason === "primary_closed" && !["refunded", "declined"].includes(String(member.status))) membershipFail("named_seat_refund_not_eligible");
|
|
4499
4864
|
const entitlement = seat.entitlementId ? await membershipLoad(db, "networkEntitlement", String(seat.entitlementId)) : void 0;
|
|
4500
4865
|
await db.transact([
|
|
4501
4866
|
membershipUpdate("networkSeat", input.seatId, { status: "refund_pending", refundReason: input.reason, revision: Number(seat.revision) + 1, updatedAt: now }),
|
|
@@ -4562,11 +4927,24 @@ async function confirmNamedSeatProviderRefund(db, input, now) {
|
|
|
4562
4927
|
const row2 = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
|
|
4563
4928
|
if (!row2) return membershipFail("named_seat_refund_evidence_pending");
|
|
4564
4929
|
const fact = row2.fact;
|
|
4565
|
-
|
|
4930
|
+
await commercialEvidenceGuard(db, input.eventId, row2.digest, "named_seat_refund_evidence_quarantined");
|
|
4566
4931
|
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
4932
|
await requestNamedSeatRefund(db, { ...input, reason: "provider_refund" }, now);
|
|
4568
4933
|
}
|
|
4569
4934
|
|
|
4935
|
+
// src/membership-seat-operator-refund.ts
|
|
4936
|
+
async function refundNamedSeatByOperator(db, input, stripeSecretKey, now) {
|
|
4937
|
+
const seat = await membershipLoad(db, "networkSeat", input.seatId);
|
|
4938
|
+
requireMembershipScope(seat, input);
|
|
4939
|
+
await requestNamedSeatRefund(db, { ...input, reason: "operator" }, now);
|
|
4940
|
+
await completeNamedSeatRefund(db, input.seatId, stripeSecretKey, now);
|
|
4941
|
+
const refunded = await membershipLoad(db, "networkSeat", input.seatId);
|
|
4942
|
+
if (refunded?.status !== "refunded" || !Number.isSafeInteger(refunded.refundedCents) || Number(refunded.refundedCents) <= 0) {
|
|
4943
|
+
throw new Error("named seat refund unconfirmed");
|
|
4944
|
+
}
|
|
4945
|
+
return { seatId: String(seat.id), status: "refunded", refundedCents: Number(refunded.refundedCents), subscriptionCanceled: false };
|
|
4946
|
+
}
|
|
4947
|
+
|
|
4570
4948
|
// src/membership-seat-offer.ts
|
|
4571
4949
|
async function namedSeatOfferDigest(offer) {
|
|
4572
4950
|
const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify([
|
|
@@ -4579,7 +4957,7 @@ async function namedSeatOfferDigest(offer) {
|
|
|
4579
4957
|
"named-only",
|
|
4580
4958
|
"full-upfront",
|
|
4581
4959
|
"auto-renew-coterm",
|
|
4582
|
-
"
|
|
4960
|
+
"no-automatic-refund"
|
|
4583
4961
|
])));
|
|
4584
4962
|
return [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
4585
4963
|
}
|
|
@@ -4619,7 +4997,7 @@ async function readNamedSeatInvitation(db, input, now) {
|
|
|
4619
4997
|
await loadMembershipAuthorityState(db, target);
|
|
4620
4998
|
const member = await membershipLoad(db, "networkMembership", String(seat.primaryMembershipId));
|
|
4621
4999
|
requireMembershipScope(member, target);
|
|
4622
|
-
if (!["pending_acceptance", "pending_approval", "pending_primary", "active"].includes(String(seat.status)) || !["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) || now >= Number(member.termEndsAt)
|
|
5000
|
+
if (!["pending_acceptance", "pending_approval", "pending_primary", "active"].includes(String(seat.status)) || !["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) || now >= Number(member.termEndsAt)) membershipFail("named_seat_invitation_unavailable");
|
|
4623
5001
|
return {
|
|
4624
5002
|
seatId: input.seatId,
|
|
4625
5003
|
primaryApplicationId: target.applicationId,
|
|
@@ -4649,7 +5027,7 @@ async function readMembershipChapterEffect(db, input, now) {
|
|
|
4649
5027
|
requireMembershipScope(member, input);
|
|
4650
5028
|
if (!effect || effect.kind !== input.kind || effect.seatId !== seat.id || effect.membershipId !== member.id || ["chapterId", "environment", "runtime"].some((key3) => effect[key3] !== seat[key3]) || !["invite_named_recipient", "grant_named_recipient_access", "revoke_named_recipient_access"].includes(input.kind)) throw new Error("invalid chapter effect");
|
|
4651
5029
|
let applicable;
|
|
4652
|
-
if (input.kind === "invite_named_recipient") applicable = seat.status === "pending_acceptance" && seat.acceptedAt === void 0 && now < Number(
|
|
5030
|
+
if (input.kind === "invite_named_recipient") applicable = seat.status === "pending_acceptance" && seat.acceptedAt === void 0 && now < Number(member.termEndsAt) && ["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status));
|
|
4653
5031
|
else if (input.kind === "revoke_named_recipient_access") applicable = ["refund_pending", "refunded"].includes(String(seat.status));
|
|
4654
5032
|
else applicable = (await readNamedSeatAccess(db, {
|
|
4655
5033
|
...input,
|
|
@@ -4678,6 +5056,7 @@ var MEMBERSHIP_SEAT_ACTION_FIELDS = {
|
|
|
4678
5056
|
"seat-reservations": ["recipientName", "recipientEmail", "purchaserEmail", "purchaserUserId"],
|
|
4679
5057
|
"seat-payments": ["seatId", "eventId"],
|
|
4680
5058
|
"seat-refunds": ["seatId", "eventId"],
|
|
5059
|
+
"seat-operator-refund": ["seatId"],
|
|
4681
5060
|
"seat-accept": recipient,
|
|
4682
5061
|
"seat-approve": recipient,
|
|
4683
5062
|
"seat-decline": recipient,
|
|
@@ -4746,6 +5125,7 @@ async function handleMembershipSeatAction(action, db, peer, input, now, stripeSe
|
|
|
4746
5125
|
if (refunded?.status !== "refunded" || refunded.refundedCents !== seat.amountCents) throw new Error("named seat refund unconfirmed");
|
|
4747
5126
|
return { seatId: seat.id, status: "refunded", refundedCents: refunded.refundedCents, subscriptionCanceled: false };
|
|
4748
5127
|
}
|
|
5128
|
+
if (action === "seat-operator-refund") return refundNamedSeatByOperator(db, { ...target, seatId: String(input.seatId) }, secret, now);
|
|
4749
5129
|
if (action === "seat-checkouts") return prepareNamedMembershipSeatPayment(db, {
|
|
4750
5130
|
...target,
|
|
4751
5131
|
priceId: peer.seatPriceId,
|
|
@@ -4834,7 +5214,7 @@ function createMembershipAuthorityHandler(options) {
|
|
|
4834
5214
|
if (req.method !== "POST") return respond({ error: "method_not_allowed" }, 405);
|
|
4835
5215
|
const sender = req.headers.get(FEDERATION_HEADERS.sender);
|
|
4836
5216
|
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);
|
|
5217
|
+
if (!["offers", "offer-preview", "checkouts", "payments", "approvals", "refunds", "renewals", "access", "restarts", "cancel-renewal", ...Object.keys(MEMBERSHIP_SEAT_ACTION_FIELDS)].includes(action ?? "")) return respond({ error: "unauthorized" }, 401);
|
|
4838
5218
|
const peer = peers.find((candidate) => candidate.appId === sender && url.pathname === `${MEMBERSHIP_AUTHORITY_PATH}/${candidate.runtime}/${action}` && !url.search);
|
|
4839
5219
|
if (!peer) return respond({ error: "unauthorized" }, 401);
|
|
4840
5220
|
let body;
|
|
@@ -4856,13 +5236,23 @@ function createMembershipAuthorityHandler(options) {
|
|
|
4856
5236
|
let input;
|
|
4857
5237
|
try {
|
|
4858
5238
|
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] ?? []];
|
|
5239
|
+
const fields = action === "seat-invitation" ? MEMBERSHIP_SEAT_ACTION_FIELDS[action] : action === "offer-preview" ? ["skuId"] : ["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
5240
|
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
5241
|
throw new TypeError("invalid request");
|
|
4862
5242
|
}
|
|
4863
5243
|
} catch {
|
|
4864
5244
|
return respond({ error: "invalid_offer_request" }, 400);
|
|
4865
5245
|
}
|
|
5246
|
+
if (action === "offer-preview") {
|
|
5247
|
+
return respond({ ok: true, preview: await previewMembershipCommercialOffer(db, {
|
|
5248
|
+
appId: peer.appId,
|
|
5249
|
+
chapterId: peer.chapterId,
|
|
5250
|
+
environment: peer.environment,
|
|
5251
|
+
runtime: peer.runtime,
|
|
5252
|
+
skuId: String(input.skuId),
|
|
5253
|
+
now: options.now?.() ?? Date.now()
|
|
5254
|
+
}) });
|
|
5255
|
+
}
|
|
4866
5256
|
if (Object.hasOwn(MEMBERSHIP_SEAT_ACTION_FIELDS, action)) {
|
|
4867
5257
|
return respond({ ok: true, seat: await handleMembershipSeatAction(
|
|
4868
5258
|
action,
|
|
@@ -4881,6 +5271,15 @@ function createMembershipAuthorityHandler(options) {
|
|
|
4881
5271
|
applicationId: String(input.applicationId),
|
|
4882
5272
|
skuId: String(input.skuId)
|
|
4883
5273
|
};
|
|
5274
|
+
if (action === "restarts" || action === "cancel-renewal") {
|
|
5275
|
+
const stripeSecret = await getVaultSecret(db, options.stripeSecretName ?? "stripe_secret_key");
|
|
5276
|
+
if (!stripeSecret) return respond({ error: "membership_authority_unavailable" }, 503);
|
|
5277
|
+
if (action === "cancel-renewal") return respond({ ok: true, renewal: await cancelMembershipAtTermEnd(db, target, options.now?.() ?? Date.now(), stripeSecret) });
|
|
5278
|
+
return respond({ ok: true, restart: await prepareMembershipRestart(db, {
|
|
5279
|
+
...target,
|
|
5280
|
+
previousSubscriptionId: String(input.previousSubscriptionId)
|
|
5281
|
+
}, options.now?.() ?? Date.now(), stripeSecret) });
|
|
5282
|
+
}
|
|
4884
5283
|
if (action === "access") return respond({ ok: true, access: await readMembershipAccess(db, target, options.now?.() ?? Date.now()) });
|
|
4885
5284
|
if (action === "renewals") return respond({ ok: true, renewal: await reconcileMembershipBilling(
|
|
4886
5285
|
db,
|
|
@@ -5004,6 +5403,12 @@ async function requestMembershipCommercialOffer(options) {
|
|
|
5004
5403
|
lifetimeMs: 6e4
|
|
5005
5404
|
});
|
|
5006
5405
|
}
|
|
5406
|
+
async function requestMembershipOfferPreview(options) {
|
|
5407
|
+
const result = await requestAuthority(options, "offer-preview", { skuId: options.skuId });
|
|
5408
|
+
const preview = result.preview;
|
|
5409
|
+
if (!preview || preview.skuId !== options.skuId || typeof preview.founding !== "boolean" || !Number.isSafeInteger(preview.basePriceCents) || !Number.isSafeInteger(preview.dueTodayCents) || preview.dueTodayCents < 0 || preview.dueTodayCents > preview.basePriceCents || !Number.isSafeInteger(preview.renewalCents) || !/^[a-z]{3}$/.test(preview.currency) || !["month", "year"].includes(preview.interval) || preview.cohortLabel !== void 0 && typeof preview.cohortLabel !== "string" || preview.discountDuration !== void 0 && !["first_term", "while_active"].includes(preview.discountDuration)) throw new Error("invalid offer preview");
|
|
5410
|
+
return preview;
|
|
5411
|
+
}
|
|
5007
5412
|
async function requestMembershipCheckoutBinding(options) {
|
|
5008
5413
|
const result = await requestAuthority(options, "checkouts", {
|
|
5009
5414
|
applicationId: options.applicationId,
|
|
@@ -5042,6 +5447,12 @@ async function requestNamedSeatDecline(options) {
|
|
|
5042
5447
|
if (!seat || seat.seatId !== options.seatId || seat.status !== "refunded" || seat.subscriptionCanceled !== false || !Number.isSafeInteger(seat.refundedCents) || seat.refundedCents <= 0) throw new Error("invalid named seat refund receipt");
|
|
5043
5448
|
return seat;
|
|
5044
5449
|
}
|
|
5450
|
+
async function requestNamedSeatOperatorRefund(options) {
|
|
5451
|
+
const result = await requestAuthority(options, "seat-operator-refund", { applicationId: options.applicationId, skuId: options.skuId, seatId: options.seatId });
|
|
5452
|
+
const seat = result.seat;
|
|
5453
|
+
if (!seat || seat.seatId !== options.seatId || seat.status !== "refunded" || seat.subscriptionCanceled !== false || !Number.isSafeInteger(seat.refundedCents) || seat.refundedCents <= 0) throw new Error("invalid named seat refund receipt");
|
|
5454
|
+
return seat;
|
|
5455
|
+
}
|
|
5045
5456
|
async function requestNamedSeatReservation(options) {
|
|
5046
5457
|
const result = await requestAuthority(options, "seat-reservations", {
|
|
5047
5458
|
applicationId: options.applicationId,
|
|
@@ -5154,7 +5565,7 @@ async function requestMembershipRefund(options) {
|
|
|
5154
5565
|
async function requestMembershipRenewal(options) {
|
|
5155
5566
|
const result = await requestAuthority(options, "renewals", { applicationId: options.applicationId, skuId: options.skuId, eventId: options.eventId });
|
|
5156
5567
|
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)) {
|
|
5568
|
+
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
5569
|
throw new Error("invalid membership renewal receipt");
|
|
5159
5570
|
}
|
|
5160
5571
|
return { membershipId: row2.membershipId, status: row2.status, termEndsAt: row2.termEndsAt, graceEndsAt: row2.graceEndsAt, autoRenew: row2.autoRenew };
|
|
@@ -5196,9 +5607,25 @@ async function requestMembershipAccess(options) {
|
|
|
5196
5607
|
const result = await requestAuthority(options, "access", { applicationId: options.applicationId, skuId: options.skuId });
|
|
5197
5608
|
return verifyMembershipAccessReceipt(result.access, options, Date.now());
|
|
5198
5609
|
}
|
|
5610
|
+
async function requestMembershipRestart(options) {
|
|
5611
|
+
const result = await requestAuthority(options, "restarts", {
|
|
5612
|
+
applicationId: options.applicationId,
|
|
5613
|
+
skuId: options.skuId,
|
|
5614
|
+
previousSubscriptionId: options.previousSubscriptionId
|
|
5615
|
+
});
|
|
5616
|
+
const row2 = result.restart;
|
|
5617
|
+
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");
|
|
5618
|
+
return { applicationId: row2.applicationId, membershipId: row2.membershipId, restartId: row2.restartId, memberNumber: row2.memberNumber, status: "ready" };
|
|
5619
|
+
}
|
|
5620
|
+
async function requestMembershipCancellation(options) {
|
|
5621
|
+
const result = await requestAuthority(options, "cancel-renewal", { applicationId: options.applicationId, skuId: options.skuId });
|
|
5622
|
+
const row2 = result.renewal;
|
|
5623
|
+
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");
|
|
5624
|
+
return { membershipId: row2.membershipId, status: row2.status, termEndsAt: row2.termEndsAt, graceEndsAt: 0, autoRenew: false };
|
|
5625
|
+
}
|
|
5199
5626
|
|
|
5200
5627
|
// src/membership-seat-cancel.ts
|
|
5201
|
-
import { initStripe, stripeRequest as
|
|
5628
|
+
import { initStripe, stripeRequest as stripeRequest9 } from "@odla-ai/stripe";
|
|
5202
5629
|
async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
|
|
5203
5630
|
membershipClock(now);
|
|
5204
5631
|
const state = await loadMembershipAuthorityState(db, target);
|
|
@@ -5226,7 +5653,7 @@ async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
|
|
|
5226
5653
|
}
|
|
5227
5654
|
const metadata = { ...stripePartitionMetadata(target), applicationId: target.applicationId, membershipId: seat.primaryMembershipId, seatId: seat.id };
|
|
5228
5655
|
const request = async (method, path, params) => {
|
|
5229
|
-
const result = await
|
|
5656
|
+
const result = await stripeRequest9(
|
|
5230
5657
|
{ secretKey },
|
|
5231
5658
|
method,
|
|
5232
5659
|
path,
|
|
@@ -5270,7 +5697,7 @@ async function drainMembershipEffects(db, options, now) {
|
|
|
5270
5697
|
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100 || !options.stripeSecretKey) throw new TypeError("invalid membership recovery configuration");
|
|
5271
5698
|
await loadMembershipAuthorityState(db, scope2);
|
|
5272
5699
|
const matches2 = (row2) => Object.entries(scope2).every(([key3, value]) => row2[key3] === value);
|
|
5273
|
-
let
|
|
5700
|
+
let pending = 0, complete = 0, more = false;
|
|
5274
5701
|
for (const status2 of ["payment_pending", "payment_processing", "payment_canceling"]) {
|
|
5275
5702
|
const seats = (await db.query({ networkSeat: { $: { where: { ...scope2, status: status2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkSeat ?? [];
|
|
5276
5703
|
more ||= seats.length > limit;
|
|
@@ -5288,26 +5715,6 @@ async function drainMembershipEffects(db, options, now) {
|
|
|
5288
5715
|
}
|
|
5289
5716
|
}
|
|
5290
5717
|
}
|
|
5291
|
-
for (const status2 of ["renewal_pending", "pending_acceptance"]) {
|
|
5292
|
-
const seats = (await db.query({ networkSeat: { $: { where: { ...scope2, status: status2 }, order: { acceptanceEndsAt: "asc" }, limit: limit + 1 } } })).networkSeat ?? [];
|
|
5293
|
-
more ||= seats.length > limit;
|
|
5294
|
-
for (const seat of seats.slice(0, limit)) {
|
|
5295
|
-
if (!matches2(seat)) throw new Error("membership recovery scope mismatch");
|
|
5296
|
-
if (seat.acceptedAt !== void 0 || now < Number(seat.acceptanceEndsAt)) continue;
|
|
5297
|
-
try {
|
|
5298
|
-
await requestNamedSeatRefund(db, {
|
|
5299
|
-
...scope2,
|
|
5300
|
-
applicationId: String(seat.applicationId),
|
|
5301
|
-
skuId: String(seat.skuId),
|
|
5302
|
-
seatId: String(seat.id),
|
|
5303
|
-
reason: "expired"
|
|
5304
|
-
}, now);
|
|
5305
|
-
expired++;
|
|
5306
|
-
} catch {
|
|
5307
|
-
pending++;
|
|
5308
|
-
}
|
|
5309
|
-
}
|
|
5310
|
-
}
|
|
5311
5718
|
const effects = (await db.query({ membershipEffect: { $: { where: {
|
|
5312
5719
|
chapterId: scope2.chapterId,
|
|
5313
5720
|
environment: scope2.environment,
|
|
@@ -5369,7 +5776,7 @@ async function drainMembershipEffects(db, options, now) {
|
|
|
5369
5776
|
pending++;
|
|
5370
5777
|
}
|
|
5371
5778
|
}
|
|
5372
|
-
return { complete, pending,
|
|
5779
|
+
return { complete, pending, more };
|
|
5373
5780
|
}
|
|
5374
5781
|
|
|
5375
5782
|
// src/schema-membership-offers.ts
|
|
@@ -5463,7 +5870,27 @@ var membershipAuthorityEntities = {
|
|
|
5463
5870
|
cancellationRequestedAt: attr9("number", true),
|
|
5464
5871
|
graceEndsAt: attr9("number", true),
|
|
5465
5872
|
...times(),
|
|
5466
|
-
refundRequestedAt: attr9("number", true)
|
|
5873
|
+
refundRequestedAt: attr9("number", true),
|
|
5874
|
+
restartId: key2(true)
|
|
5875
|
+
} },
|
|
5876
|
+
membershipRestart: { attrs: {
|
|
5877
|
+
id: id9(),
|
|
5878
|
+
...scope(),
|
|
5879
|
+
membershipId: key2(),
|
|
5880
|
+
restartId: key2(),
|
|
5881
|
+
previousSubscriptionId: key2(),
|
|
5882
|
+
memberNumber: attr9("number"),
|
|
5883
|
+
status: key2(),
|
|
5884
|
+
createdAt: attr9("number")
|
|
5885
|
+
} },
|
|
5886
|
+
membershipPurchaseHistory: { attrs: {
|
|
5887
|
+
id: id9(),
|
|
5888
|
+
...scope(),
|
|
5889
|
+
membershipId: key2(),
|
|
5890
|
+
providerSubscriptionId: key2(),
|
|
5891
|
+
memberSnapshot: attr9("json"),
|
|
5892
|
+
bindingSnapshot: attr9("json"),
|
|
5893
|
+
archivedAt: attr9("number")
|
|
5467
5894
|
} },
|
|
5468
5895
|
networkEntitlement: { attrs: {
|
|
5469
5896
|
id: id9(),
|
|
@@ -6894,9 +7321,9 @@ function sharedRecordFromCrm(crm, record4, target, sourceId) {
|
|
|
6894
7321
|
}
|
|
6895
7322
|
const nameField = def.nameField ?? "name";
|
|
6896
7323
|
const input = {};
|
|
6897
|
-
for (const
|
|
6898
|
-
const value = record4.fields?.[
|
|
6899
|
-
if (value !== void 0) input[
|
|
7324
|
+
for (const field2 of /* @__PURE__ */ new Set([nameField, ...fields])) {
|
|
7325
|
+
const value = record4.fields?.[field2];
|
|
7326
|
+
if (value !== void 0) input[field2] = value;
|
|
6900
7327
|
}
|
|
6901
7328
|
if (input[nameField] === void 0) input[nameField] = record4.name;
|
|
6902
7329
|
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 +7592,9 @@ function subAnnualCents(sub) {
|
|
|
7165
7592
|
}
|
|
7166
7593
|
|
|
7167
7594
|
// src/payments-stripe.ts
|
|
7168
|
-
import { stripeRequest as
|
|
7595
|
+
import { stripeRequest as stripeRequest10, verifyStripeSignature as verifyStripeSignature2 } from "@odla-ai/stripe";
|
|
7169
7596
|
async function stripeCall(sk, method, path, params, idempotencyKey) {
|
|
7170
|
-
return
|
|
7597
|
+
return stripeRequest10(
|
|
7171
7598
|
{ secretKey: sk },
|
|
7172
7599
|
method,
|
|
7173
7600
|
path,
|
|
@@ -7477,7 +7904,8 @@ function applicationSummary(app) {
|
|
|
7477
7904
|
status: app.status,
|
|
7478
7905
|
createdAt: app.createdAt ?? null,
|
|
7479
7906
|
meetingLink: app.meetingLink ?? null,
|
|
7480
|
-
paid: Boolean(app.stripeSubscriptionId) && app.status !== "refunded",
|
|
7907
|
+
paid: Boolean(app.stripeSubscriptionId) && app.status !== "refunded" && app.canceled !== true && !["lapsed", "refunded", "refund_pending", "canceled", "restarting"].includes(app.membershipStatus ?? ""),
|
|
7908
|
+
...app.membershipStatus ? { membershipStatus: app.membershipStatus, membershipGraceEndsAt: app.membershipGraceEndsAt, membershipAutoRenew: app.membershipAutoRenew } : {},
|
|
7481
7909
|
renewalAt: app.renewalAt ?? null,
|
|
7482
7910
|
canceled: app.canceled === true,
|
|
7483
7911
|
tierId: app.tierId ?? null
|
|
@@ -7659,7 +8087,7 @@ function isValidTimeZone(tz) {
|
|
|
7659
8087
|
function resolveScheduling(config) {
|
|
7660
8088
|
const result = validateScheduling(config);
|
|
7661
8089
|
if (result.ok) return result.value;
|
|
7662
|
-
const detail = Object.entries(result.errors).map(([
|
|
8090
|
+
const detail = Object.entries(result.errors).map(([field2, message]) => `${field2}: ${message}`).join(" ");
|
|
7663
8091
|
throw new Error(`scheduling: ${detail}`);
|
|
7664
8092
|
}
|
|
7665
8093
|
function validateScheduling(config) {
|
|
@@ -8806,7 +9234,7 @@ var MEMBER_SELF_READABLE_FIELDS = new Set(
|
|
|
8806
9234
|
MEMBER_SYSTEM_AUTHORITY_MATRIX.filter((row2) => row2.memberAccess !== "none").map((row2) => row2.id)
|
|
8807
9235
|
);
|
|
8808
9236
|
function projectMemberSelf(record4) {
|
|
8809
|
-
return Object.fromEntries(Object.entries(record4).filter(([
|
|
9237
|
+
return Object.fromEntries(Object.entries(record4).filter(([field2]) => MEMBER_SELF_READABLE_FIELDS.has(field2)));
|
|
8810
9238
|
}
|
|
8811
9239
|
function classifyMemberSystemWrite(intent) {
|
|
8812
9240
|
if (intent.actor === "bnf") return "reject_bnf_surface";
|
|
@@ -9132,6 +9560,7 @@ export {
|
|
|
9132
9560
|
planMemberSystemEventReplay,
|
|
9133
9561
|
planMemberSystemMigration,
|
|
9134
9562
|
planPrivateRoleMigration,
|
|
9563
|
+
previewMembershipCommercialOffer,
|
|
9135
9564
|
projectApplicant,
|
|
9136
9565
|
projectBuilderLibraryDelivery,
|
|
9137
9566
|
projectMemberContent,
|
|
@@ -9160,12 +9589,15 @@ export {
|
|
|
9160
9589
|
renewalPatch,
|
|
9161
9590
|
requestMembershipAccess,
|
|
9162
9591
|
requestMembershipApproval,
|
|
9592
|
+
requestMembershipCancellation,
|
|
9163
9593
|
requestMembershipChapterEffect,
|
|
9164
9594
|
requestMembershipCheckoutBinding,
|
|
9165
9595
|
requestMembershipCommercialOffer,
|
|
9596
|
+
requestMembershipOfferPreview,
|
|
9166
9597
|
requestMembershipPayment,
|
|
9167
9598
|
requestMembershipRefund,
|
|
9168
9599
|
requestMembershipRenewal,
|
|
9600
|
+
requestMembershipRestart,
|
|
9169
9601
|
requestNamedSeatAcceptance,
|
|
9170
9602
|
requestNamedSeatAccess,
|
|
9171
9603
|
requestNamedSeatApproval,
|
|
@@ -9173,6 +9605,7 @@ export {
|
|
|
9173
9605
|
requestNamedSeatDecline,
|
|
9174
9606
|
requestNamedSeatInvitation,
|
|
9175
9607
|
requestNamedSeatOffer,
|
|
9608
|
+
requestNamedSeatOperatorRefund,
|
|
9176
9609
|
requestNamedSeatPayment,
|
|
9177
9610
|
requestNamedSeatRefund2 as requestNamedSeatRefund,
|
|
9178
9611
|
requestNamedSeatReservation,
|