@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.cjs
CHANGED
|
@@ -176,6 +176,7 @@ __export(src_exports, {
|
|
|
176
176
|
planMemberSystemEventReplay: () => planMemberSystemEventReplay,
|
|
177
177
|
planMemberSystemMigration: () => planMemberSystemMigration,
|
|
178
178
|
planPrivateRoleMigration: () => planPrivateRoleMigration,
|
|
179
|
+
previewMembershipCommercialOffer: () => previewMembershipCommercialOffer,
|
|
179
180
|
projectApplicant: () => projectApplicant,
|
|
180
181
|
projectBuilderLibraryDelivery: () => projectBuilderLibraryDelivery,
|
|
181
182
|
projectMemberContent: () => projectMemberContent,
|
|
@@ -204,12 +205,15 @@ __export(src_exports, {
|
|
|
204
205
|
renewalPatch: () => renewalPatch,
|
|
205
206
|
requestMembershipAccess: () => requestMembershipAccess,
|
|
206
207
|
requestMembershipApproval: () => requestMembershipApproval,
|
|
208
|
+
requestMembershipCancellation: () => requestMembershipCancellation,
|
|
207
209
|
requestMembershipChapterEffect: () => requestMembershipChapterEffect,
|
|
208
210
|
requestMembershipCheckoutBinding: () => requestMembershipCheckoutBinding,
|
|
209
211
|
requestMembershipCommercialOffer: () => requestMembershipCommercialOffer,
|
|
212
|
+
requestMembershipOfferPreview: () => requestMembershipOfferPreview,
|
|
210
213
|
requestMembershipPayment: () => requestMembershipPayment,
|
|
211
214
|
requestMembershipRefund: () => requestMembershipRefund,
|
|
212
215
|
requestMembershipRenewal: () => requestMembershipRenewal,
|
|
216
|
+
requestMembershipRestart: () => requestMembershipRestart,
|
|
213
217
|
requestNamedSeatAcceptance: () => requestNamedSeatAcceptance,
|
|
214
218
|
requestNamedSeatAccess: () => requestNamedSeatAccess,
|
|
215
219
|
requestNamedSeatApproval: () => requestNamedSeatApproval,
|
|
@@ -217,6 +221,7 @@ __export(src_exports, {
|
|
|
217
221
|
requestNamedSeatDecline: () => requestNamedSeatDecline,
|
|
218
222
|
requestNamedSeatInvitation: () => requestNamedSeatInvitation,
|
|
219
223
|
requestNamedSeatOffer: () => requestNamedSeatOffer,
|
|
224
|
+
requestNamedSeatOperatorRefund: () => requestNamedSeatOperatorRefund,
|
|
220
225
|
requestNamedSeatPayment: () => requestNamedSeatPayment,
|
|
221
226
|
requestNamedSeatRefund: () => requestNamedSeatRefund2,
|
|
222
227
|
requestNamedSeatReservation: () => requestNamedSeatReservation,
|
|
@@ -241,7 +246,7 @@ __export(src_exports, {
|
|
|
241
246
|
slotWindow: () => slotWindow,
|
|
242
247
|
stageIndex: () => stageIndex,
|
|
243
248
|
stripeCall: () => stripeCall,
|
|
244
|
-
stripeForm: () =>
|
|
249
|
+
stripeForm: () => import_stripe10.stripeForm,
|
|
245
250
|
subAnnualCents: () => subAnnualCents,
|
|
246
251
|
submitApplication: () => submitApplication,
|
|
247
252
|
subscriptionIdempotencyKey: () => subscriptionIdempotencyKey,
|
|
@@ -262,7 +267,7 @@ __export(src_exports, {
|
|
|
262
267
|
verifyMembershipAccessReceipt: () => verifyMembershipAccessReceipt,
|
|
263
268
|
verifyMembershipCheckoutBinding: () => verifyMembershipCheckoutBinding,
|
|
264
269
|
verifyMembershipPaymentReceipt: () => verifyMembershipPaymentReceipt,
|
|
265
|
-
verifyStripeSignature: () =>
|
|
270
|
+
verifyStripeSignature: () => import_stripe10.verifyStripeSignature,
|
|
266
271
|
webhookMutationId: () => webhookMutationId
|
|
267
272
|
});
|
|
268
273
|
module.exports = __toCommonJS(src_exports);
|
|
@@ -307,12 +312,12 @@ function resolveApplication(a) {
|
|
|
307
312
|
if (!Array.isArray(arr) || !arr.every((f) => typeof f === "string" && f !== "")) {
|
|
308
313
|
throw new Error(`defineChapter.application.${name}: must be an array of field-name strings`);
|
|
309
314
|
}
|
|
310
|
-
const unsafe = arr.find((
|
|
315
|
+
const unsafe = arr.find((field2) => !SCHEMA_FIELD.test(field2));
|
|
311
316
|
if (unsafe) {
|
|
312
317
|
throw new Error(`defineChapter.application.${name}: field "${unsafe}" must match ${SCHEMA_FIELD.source}`);
|
|
313
318
|
}
|
|
314
319
|
}
|
|
315
|
-
const duplicate = [...required, ...optional].find((
|
|
320
|
+
const duplicate = [...required, ...optional].find((field2, index2, fields) => fields.indexOf(field2) !== index2);
|
|
316
321
|
if (duplicate) {
|
|
317
322
|
throw new Error(`defineChapter.application: duplicate field "${duplicate}" across required/optional lists`);
|
|
318
323
|
}
|
|
@@ -323,14 +328,14 @@ function resolveApplication(a) {
|
|
|
323
328
|
throw new Error("defineChapter.application.crmFields: must be an array of field-name strings");
|
|
324
329
|
}
|
|
325
330
|
const conditions = a?.conditions ?? {};
|
|
326
|
-
for (const [
|
|
331
|
+
for (const [field2, declared] of Object.entries(conditions)) {
|
|
327
332
|
for (const key3 of ["visibleWhen", "requiredWhen"]) {
|
|
328
333
|
const expression = declared?.[key3];
|
|
329
334
|
if (expression === void 0) continue;
|
|
330
335
|
if (typeof expression !== "string" || !expression.trim()) {
|
|
331
|
-
throw new Error(`defineChapter.application.conditions.${
|
|
336
|
+
throw new Error(`defineChapter.application.conditions.${field2}.${key3}: must be a non-empty CEL condition`);
|
|
332
337
|
}
|
|
333
|
-
(0, import_crm.assertFieldCondition)(expression, `defineChapter.application.conditions.${
|
|
338
|
+
(0, import_crm.assertFieldCondition)(expression, `defineChapter.application.conditions.${field2}.${key3}`);
|
|
334
339
|
}
|
|
335
340
|
}
|
|
336
341
|
return {
|
|
@@ -401,6 +406,13 @@ async function submitApplication(db, chapter, fields, opts) {
|
|
|
401
406
|
if (opts.groupId) row2.groupId = opts.groupId;
|
|
402
407
|
if (opts.runtime) row2.stripeRuntime = opts.runtime;
|
|
403
408
|
if (chapter.account !== "none") row2.clerkAccountPending = true;
|
|
409
|
+
if (opts.namedSeat) Object.assign(row2, {
|
|
410
|
+
namedSeatId: opts.namedSeat.seatId,
|
|
411
|
+
namedSeatPrimaryApplicationId: opts.namedSeat.primaryApplicationId,
|
|
412
|
+
namedSeatClaimPending: true,
|
|
413
|
+
clerkUserId: opts.namedSeat.userId,
|
|
414
|
+
clerkAccountPending: false
|
|
415
|
+
});
|
|
404
416
|
if (typeof fields.email === "string" && isValidEmail(fields.email.trim())) {
|
|
405
417
|
row2.emailNormalized = normalizeChapterEmail(fields.email);
|
|
406
418
|
row2.signupEmailKey = row2.emailNormalized;
|
|
@@ -530,6 +542,10 @@ var applicationAttrs = {
|
|
|
530
542
|
giftPaidAt: attr("number", { optional: true, indexed: true }),
|
|
531
543
|
giftRevokedAt: attr("number", { optional: true, indexed: true }),
|
|
532
544
|
giftCheckoutStartedAt: attr("number", { optional: true, indexed: true }),
|
|
545
|
+
membershipRestartId: attr("string", { optional: true, indexed: true }),
|
|
546
|
+
membershipStatus: attr("string", { optional: true }),
|
|
547
|
+
membershipGraceEndsAt: attr("number", { optional: true }),
|
|
548
|
+
membershipAutoRenew: attr("boolean", { optional: true }),
|
|
533
549
|
namedSeatId: attr("string", { optional: true, unique: true, indexed: true }),
|
|
534
550
|
namedSeatPrimaryApplicationId: attr("string", { optional: true }),
|
|
535
551
|
namedSeatClaimPending: attr("boolean", { optional: true, indexed: true }),
|
|
@@ -540,19 +556,19 @@ function applicationsFor(application) {
|
|
|
540
556
|
for (const [name, spec] of Object.entries(applicationAttrs)) {
|
|
541
557
|
attrs[name] = { ...spec, optional: APPLICATION_INPUT_FIELDS.has(name) ? true : spec.optional };
|
|
542
558
|
}
|
|
543
|
-
const apply = (
|
|
544
|
-
const existing = Object.hasOwn(attrs,
|
|
545
|
-
if (existing && !APPLICATION_INPUT_FIELDS.has(
|
|
546
|
-
if (existing && existing.type !== "string") throw new Error(`defineChapter.application: field "${
|
|
547
|
-
Object.defineProperty(attrs,
|
|
559
|
+
const apply = (field2, optional) => {
|
|
560
|
+
const existing = Object.hasOwn(attrs, field2) ? attrs[field2] : void 0;
|
|
561
|
+
if (existing && !APPLICATION_INPUT_FIELDS.has(field2)) throw new Error(`defineChapter.application: field "${field2}" is reserved by the applications schema`);
|
|
562
|
+
if (existing && existing.type !== "string") throw new Error(`defineChapter.application: field "${field2}" is not a configurable string field`);
|
|
563
|
+
Object.defineProperty(attrs, field2, {
|
|
548
564
|
value: existing ? { ...existing, optional } : attr("string", { optional }),
|
|
549
565
|
enumerable: true,
|
|
550
566
|
configurable: true,
|
|
551
567
|
writable: true
|
|
552
568
|
});
|
|
553
569
|
};
|
|
554
|
-
for (const
|
|
555
|
-
for (const
|
|
570
|
+
for (const field2 of application.required) apply(field2, false);
|
|
571
|
+
for (const field2 of application.optional) apply(field2, true);
|
|
556
572
|
return { attrs };
|
|
557
573
|
}
|
|
558
574
|
|
|
@@ -759,6 +775,19 @@ var giftRedemptionAttemptsEntity = { attrs: {
|
|
|
759
775
|
updatedAt: attr4("number")
|
|
760
776
|
} };
|
|
761
777
|
|
|
778
|
+
// src/payment-recovery-schema.ts
|
|
779
|
+
var field = (type, unique = false) => ({ type, unique, indexed: type !== "json", optional: false });
|
|
780
|
+
var paymentRecoveryReviewsEntity = { attrs: {
|
|
781
|
+
id: field("string", true),
|
|
782
|
+
eventId: field("string"),
|
|
783
|
+
actorUserId: field("string"),
|
|
784
|
+
reviewedAt: field("number"),
|
|
785
|
+
previousDigest: field("string"),
|
|
786
|
+
providerDigest: field("string"),
|
|
787
|
+
scope: field("string"),
|
|
788
|
+
previousState: field("json")
|
|
789
|
+
} };
|
|
790
|
+
|
|
762
791
|
// src/schema-stripe.ts
|
|
763
792
|
var attr5 = (type, flags = {}) => ({
|
|
764
793
|
type,
|
|
@@ -779,6 +808,10 @@ var stripeEventReceiptsEntity = {
|
|
|
779
808
|
dataEnvironment: attr5("string", { indexed: true, optional: true }),
|
|
780
809
|
runtime: attr5("string", { indexed: true, optional: true }),
|
|
781
810
|
chapterId: attr5("string", { indexed: true, optional: true }),
|
|
811
|
+
retryReady: attr5("boolean", { optional: true }),
|
|
812
|
+
reviewedDigest: attr5("string", { optional: true }),
|
|
813
|
+
reviewId: attr5("string", { optional: true }),
|
|
814
|
+
collisionDigest: attr5("string", { optional: true }),
|
|
782
815
|
receivedAt: attr5("number", { indexed: true })
|
|
783
816
|
}
|
|
784
817
|
};
|
|
@@ -803,6 +836,7 @@ var subscriptionCheckoutIntentsEntity = {
|
|
|
803
836
|
requestedCustomerId: attr5("string", { indexed: true, optional: true }),
|
|
804
837
|
providerCustomerId: attr5("string", { indexed: true, optional: true }),
|
|
805
838
|
providerSubscriptionId: attr5("string", { unique: true, indexed: true, optional: true }),
|
|
839
|
+
purchaseAttemptId: attr5("string", { optional: true }),
|
|
806
840
|
status: attr5("string", { indexed: true }),
|
|
807
841
|
createdAt: attr5("number", { indexed: true }),
|
|
808
842
|
updatedAt: attr5("number", { indexed: true })
|
|
@@ -827,6 +861,20 @@ var membershipQuoteProjectionsEntity = {
|
|
|
827
861
|
quote: attr5("json", { optional: true })
|
|
828
862
|
}
|
|
829
863
|
};
|
|
864
|
+
var membershipCheckoutHistoryEntity = { attrs: {
|
|
865
|
+
id: attr5("string", { unique: true, indexed: true }),
|
|
866
|
+
applicationId: attr5("string", { indexed: true }),
|
|
867
|
+
appId: attr5("string"),
|
|
868
|
+
chapterId: attr5("string"),
|
|
869
|
+
environment: attr5("string"),
|
|
870
|
+
runtime: attr5("string"),
|
|
871
|
+
skuId: attr5("string"),
|
|
872
|
+
providerSubscriptionId: attr5("string", { indexed: true }),
|
|
873
|
+
applicationSnapshot: attr5("json"),
|
|
874
|
+
intentSnapshot: attr5("json"),
|
|
875
|
+
quoteSnapshot: attr5("json"),
|
|
876
|
+
archivedAt: attr5("number")
|
|
877
|
+
} };
|
|
830
878
|
|
|
831
879
|
// src/schema-network-person.ts
|
|
832
880
|
var string = (flags = {}) => ({
|
|
@@ -960,15 +1008,19 @@ var exceptions = { attrs: {
|
|
|
960
1008
|
kind: string2({ indexed: true }),
|
|
961
1009
|
sourceKey: string2({ indexed: true, optional: true }),
|
|
962
1010
|
status: string2({ indexed: true }),
|
|
1011
|
+
resolvedDigest: string2({ optional: true }),
|
|
1012
|
+
incomingDigest: string2({ optional: true }),
|
|
963
1013
|
detail: json2(),
|
|
964
1014
|
updatedAt: number2({ indexed: true })
|
|
965
1015
|
} };
|
|
966
1016
|
var networkCommercialEntities = {
|
|
1017
|
+
paymentRecoveryReviews: paymentRecoveryReviewsEntity,
|
|
967
1018
|
networkCommercialEvents: events,
|
|
968
1019
|
networkCommercialCustomers: projection(),
|
|
969
1020
|
networkCommercialSubscriptions: projection(),
|
|
970
1021
|
networkCommercialInvoices: projection(),
|
|
971
1022
|
networkCommercialRefunds: projection(),
|
|
1023
|
+
networkCommercialSeats: projection(),
|
|
972
1024
|
networkCommercialExceptions: exceptions
|
|
973
1025
|
};
|
|
974
1026
|
|
|
@@ -1171,8 +1223,9 @@ function chapterDb(mode, auth, includeNetworkNotes = false, application = resolv
|
|
|
1171
1223
|
entities.tiers = tiers;
|
|
1172
1224
|
entities.meetings = meetings;
|
|
1173
1225
|
entities.emailLog = emailLog;
|
|
1226
|
+
entities.paymentRecoveryReviews = paymentRecoveryReviewsEntity;
|
|
1174
1227
|
entities.stripeEventReceipts = stripeEventReceiptsEntity;
|
|
1175
|
-
entities
|
|
1228
|
+
Object.assign(entities, { subscriptionCheckoutIntents: subscriptionCheckoutIntentsEntity, membershipCheckoutHistory: membershipCheckoutHistoryEntity });
|
|
1176
1229
|
entities.membershipQuoteProjections = membershipQuoteProjectionsEntity;
|
|
1177
1230
|
entities.namedSeatConsents = namedSeatConsentsEntity;
|
|
1178
1231
|
entities.admissionGrants = admissionGrantsEntity;
|
|
@@ -1663,6 +1716,7 @@ var DEFAULT_ADMIN_COPY = {
|
|
|
1663
1716
|
},
|
|
1664
1717
|
records: {
|
|
1665
1718
|
workflowMissing: "This record is not linked to an application workflow.",
|
|
1719
|
+
invitation: "Invitation",
|
|
1666
1720
|
comms: "Comms",
|
|
1667
1721
|
commsHistory: "Comms history",
|
|
1668
1722
|
scheduling: "Scheduling",
|
|
@@ -1710,6 +1764,19 @@ var DEFAULT_COMMON_COPY = {
|
|
|
1710
1764
|
unknown: "(unknown)"
|
|
1711
1765
|
};
|
|
1712
1766
|
var DEFAULT_JOIN_COPY = {
|
|
1767
|
+
namedSeat: {
|
|
1768
|
+
title: "Your membership invitation",
|
|
1769
|
+
from: "{purchaserName} invited you.",
|
|
1770
|
+
prepaid: "Your {tierName} seat is already paid for.",
|
|
1771
|
+
body: "Accept your invitation, then complete your own application requirements. You will not be asked to pay for this seat.",
|
|
1772
|
+
consent: "I accept this membership for myself. It cannot be transferred and remains current while the purchaser's membership and my seat are current.",
|
|
1773
|
+
accept: "Accept my invitation",
|
|
1774
|
+
accepting: "Confirming your invitation\u2026",
|
|
1775
|
+
loading: "Loading your invitation\u2026",
|
|
1776
|
+
unavailable: "Your invitation is unavailable. Sign in with the invited email address, then try again.",
|
|
1777
|
+
retry: "Try again",
|
|
1778
|
+
failure: "We could not confirm acceptance. Retry this invitation; you do not need to purchase another membership."
|
|
1779
|
+
},
|
|
1713
1780
|
form: {
|
|
1714
1781
|
submit: "Submit application",
|
|
1715
1782
|
submitting: "Submitting\u2026",
|
|
@@ -1717,6 +1784,8 @@ var DEFAULT_JOIN_COPY = {
|
|
|
1717
1784
|
unexpectedFailure: "Something went wrong. Please try again."
|
|
1718
1785
|
},
|
|
1719
1786
|
booking: {
|
|
1787
|
+
title: "Book your introduction",
|
|
1788
|
+
intro: "Choose a time to meet your chapter and get to know each other.",
|
|
1720
1789
|
unavailable: "Scheduling is briefly unavailable. We'll reach out by email to arrange your call.",
|
|
1721
1790
|
loadFailed: "Times are briefly unavailable. Please try again.",
|
|
1722
1791
|
slotTaken: "That time was just taken. Here are the current openings.",
|
|
@@ -1732,7 +1801,9 @@ var DEFAULT_JOIN_COPY = {
|
|
|
1732
1801
|
processing: "Processing\u2026",
|
|
1733
1802
|
payAndContinue: "Pay and continue",
|
|
1734
1803
|
incomplete: "The payment did not complete. Please try again.",
|
|
1735
|
-
consent: "I have read and agree to the pricing and refund policy above."
|
|
1804
|
+
consent: "I have read and agree to the pricing and refund policy above.",
|
|
1805
|
+
foundingMember: "Founding member rate",
|
|
1806
|
+
foundingDiscount: "Founding discount"
|
|
1736
1807
|
},
|
|
1737
1808
|
done: {
|
|
1738
1809
|
label: "You're booked",
|
|
@@ -1908,16 +1979,16 @@ function resolveFormationFlowFields(crm, formation) {
|
|
|
1908
1979
|
if (!formation.enabled) return [];
|
|
1909
1980
|
const def = crm.type(formation.type);
|
|
1910
1981
|
return [...formation.required, ...formation.optional].map((id10, order2) => {
|
|
1911
|
-
const
|
|
1982
|
+
const field2 = def.fields[id10];
|
|
1912
1983
|
return {
|
|
1913
1984
|
id: id10,
|
|
1914
|
-
label:
|
|
1915
|
-
type:
|
|
1985
|
+
label: field2.label ?? humanize(id10),
|
|
1986
|
+
type: field2.type,
|
|
1916
1987
|
required: formation.required.includes(id10),
|
|
1917
1988
|
order: order2,
|
|
1918
1989
|
maxLength: formation.maxLen[id10] ?? formation.defaultMaxLen,
|
|
1919
|
-
...
|
|
1920
|
-
...id10 === def.emailField ||
|
|
1990
|
+
...field2.options ? { options: [...field2.options] } : {},
|
|
1991
|
+
...id10 === def.emailField || field2.type === "email" ? { emailIdentity: "literal" } : {}
|
|
1921
1992
|
};
|
|
1922
1993
|
});
|
|
1923
1994
|
}
|
|
@@ -1953,19 +2024,19 @@ function resolveLeaderFormation(input, crm) {
|
|
|
1953
2024
|
const required = input.required ?? (type === "chapter_application" ? DEFAULT_REQUIRED2 : []);
|
|
1954
2025
|
const optional = input.optional ?? (type === "chapter_application" ? DEFAULT_OPTIONAL2 : []);
|
|
1955
2026
|
for (const [name, values] of [["required", required], ["optional", optional]]) {
|
|
1956
|
-
if (!Array.isArray(values) || !values.every((
|
|
2027
|
+
if (!Array.isArray(values) || !values.every((field2) => typeof field2 === "string" && field2 !== "")) {
|
|
1957
2028
|
throw new Error(`defineChapter.formation.${name}: must be an array of field-name strings`);
|
|
1958
2029
|
}
|
|
1959
2030
|
}
|
|
1960
|
-
const duplicates = [...required, ...optional].filter((
|
|
2031
|
+
const duplicates = [...required, ...optional].filter((field2, index2, fields) => fields.indexOf(field2) !== index2);
|
|
1961
2032
|
if (duplicates.length) throw new Error(`defineChapter.formation: duplicate public field "${duplicates[0]}"`);
|
|
1962
|
-
const unknown = [...required, ...optional].filter((
|
|
2033
|
+
const unknown = [...required, ...optional].filter((field2) => !def.fields[field2]);
|
|
1963
2034
|
if (unknown.length) throw new Error(`defineChapter.formation: field "${unknown[0]}" is not declared on CRM type "${type}"`);
|
|
1964
|
-
const missingRequired = Object.entries(def.fields).filter(([,
|
|
2035
|
+
const missingRequired = Object.entries(def.fields).filter(([, field2]) => field2.required).map(([field2]) => field2).filter((field2) => !required.includes(field2));
|
|
1965
2036
|
if (missingRequired.length) {
|
|
1966
2037
|
throw new Error(`defineChapter.formation.required: must include required CRM field "${missingRequired[0]}"`);
|
|
1967
2038
|
}
|
|
1968
|
-
const unsupportedRequired = required.find((
|
|
2039
|
+
const unsupportedRequired = required.find((field2) => !["string", "email", "url", "enum"].includes(def.fields[field2].type));
|
|
1969
2040
|
if (unsupportedRequired) {
|
|
1970
2041
|
throw new Error(`defineChapter.formation.required: field "${unsupportedRequired}" must be string-like`);
|
|
1971
2042
|
}
|
|
@@ -1978,9 +2049,9 @@ function resolveLeaderFormation(input, crm) {
|
|
|
1978
2049
|
if (!Number.isSafeInteger(bodyCap) || bodyCap < 1024 || bodyCap > 131072) {
|
|
1979
2050
|
throw new Error("defineChapter.formation.bodyCap: must be an integer from 1024 to 131072");
|
|
1980
2051
|
}
|
|
1981
|
-
for (const [
|
|
1982
|
-
if (![...required, ...optional].includes(
|
|
1983
|
-
throw new Error(`defineChapter.formation.maxLen.${
|
|
2052
|
+
for (const [field2, cap] of Object.entries(input.maxLen ?? {})) {
|
|
2053
|
+
if (![...required, ...optional].includes(field2) || !Number.isSafeInteger(cap) || cap < 1 || cap > 32768) {
|
|
2054
|
+
throw new Error(`defineChapter.formation.maxLen.${field2}: must name a public field and be an integer from 1 to 32768`);
|
|
1984
2055
|
}
|
|
1985
2056
|
}
|
|
1986
2057
|
return {
|
|
@@ -2070,10 +2141,10 @@ function resolveNetwork(config, crm) {
|
|
|
2070
2141
|
}
|
|
2071
2142
|
const def = crm.config.types[type];
|
|
2072
2143
|
if (!def) throw new Error(`defineChapter.network.targets.${target.id}.fields: unknown leader CRM type "${type}"`);
|
|
2073
|
-
const unknown = names.filter((
|
|
2144
|
+
const unknown = names.filter((field2) => !def.fields[field2]);
|
|
2074
2145
|
if (unknown.length > 0) {
|
|
2075
2146
|
throw new Error(
|
|
2076
|
-
`defineChapter.network.targets.${target.id}.fields.${type}: ${unknown.map((
|
|
2147
|
+
`defineChapter.network.targets.${target.id}.fields.${type}: ${unknown.map((field2) => `"${field2}"`).join(", ")} not declared on the leader CRM type`
|
|
2077
2148
|
);
|
|
2078
2149
|
}
|
|
2079
2150
|
const nameField = def.nameField ?? "name";
|
|
@@ -2161,10 +2232,10 @@ function resolveNetwork(config, crm) {
|
|
|
2161
2232
|
if (!def) {
|
|
2162
2233
|
throw new Error(`defineChapter.network.readers.${reader.id}.fields: unknown local CRM type "${type}"`);
|
|
2163
2234
|
}
|
|
2164
|
-
const unknown = names.filter((
|
|
2235
|
+
const unknown = names.filter((field2) => !def.fields[field2]);
|
|
2165
2236
|
if (unknown.length > 0) {
|
|
2166
2237
|
throw new Error(
|
|
2167
|
-
`defineChapter.network.readers.${reader.id}.fields.${type}: ${unknown.map((
|
|
2238
|
+
`defineChapter.network.readers.${reader.id}.fields.${type}: ${unknown.map((field2) => `"${field2}"`).join(", ")} not declared on the local CRM type`
|
|
2168
2239
|
);
|
|
2169
2240
|
}
|
|
2170
2241
|
const nameField = def.nameField ?? "name";
|
|
@@ -2482,6 +2553,48 @@ function defineChapter(config) {
|
|
|
2482
2553
|
return chapter;
|
|
2483
2554
|
}
|
|
2484
2555
|
|
|
2556
|
+
// src/network-commercial-seats.ts
|
|
2557
|
+
function commercialSeatItems(object2) {
|
|
2558
|
+
const items = object2.items;
|
|
2559
|
+
if (!items || items.has_more !== false || !Array.isArray(items.data) || items.data.length > 100) return void 0;
|
|
2560
|
+
const result = [];
|
|
2561
|
+
for (const item of items.data) {
|
|
2562
|
+
const metadata = item.metadata;
|
|
2563
|
+
if (!metadata?.seatId || !metadata.membershipId) continue;
|
|
2564
|
+
const price = typeof item.price === "string" ? item.price : item.price?.id;
|
|
2565
|
+
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;
|
|
2566
|
+
result.push({
|
|
2567
|
+
seatId: metadata.seatId,
|
|
2568
|
+
membershipId: metadata.membershipId,
|
|
2569
|
+
subscriptionItemId: item.id,
|
|
2570
|
+
priceId: price,
|
|
2571
|
+
...Number.isSafeInteger(item.current_period_end) ? { currentPeriodEnd: Number(item.current_period_end) } : {}
|
|
2572
|
+
});
|
|
2573
|
+
}
|
|
2574
|
+
return result;
|
|
2575
|
+
}
|
|
2576
|
+
function projectCommercialSeats(payments, subscriptions, refunds) {
|
|
2577
|
+
return payments.map((payment) => {
|
|
2578
|
+
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 })) : []);
|
|
2579
|
+
const match = matches2.length === 1 ? matches2[0] : void 0;
|
|
2580
|
+
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);
|
|
2581
|
+
return {
|
|
2582
|
+
...payment,
|
|
2583
|
+
objectKind: "seat",
|
|
2584
|
+
objectId: payment.seatId,
|
|
2585
|
+
refundedCents,
|
|
2586
|
+
...refundedCents >= Number(payment.amountReceivedCents) && refundedCents > 0 ? { status: "refunded" } : {},
|
|
2587
|
+
...match ? {
|
|
2588
|
+
subscriptionId: match.subscription.subscriptionId,
|
|
2589
|
+
subscriptionItemId: match.item.subscriptionItemId,
|
|
2590
|
+
priceId: match.item.priceId,
|
|
2591
|
+
currentPeriodEnd: match.item.currentPeriodEnd ?? match.subscription.currentPeriodEnd,
|
|
2592
|
+
cancelAtPeriodEnd: match.subscription.cancelAtPeriodEnd
|
|
2593
|
+
} : {}
|
|
2594
|
+
};
|
|
2595
|
+
});
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2485
2598
|
// src/subscription-billing-terms.ts
|
|
2486
2599
|
var import_stripe = require("@odla-ai/stripe");
|
|
2487
2600
|
|
|
@@ -2653,6 +2766,7 @@ function normalizeNetworkCommercialStripeEvent(input, digest) {
|
|
|
2653
2766
|
...chargeId ? { chargeId } : {},
|
|
2654
2767
|
...paymentIntentId ? { paymentIntentId } : {},
|
|
2655
2768
|
...price,
|
|
2769
|
+
...objectKind === "subscription" && commercialSeatItems(object2) ? { seatSubscriptionItems: commercialSeatItems(object2) } : {},
|
|
2656
2770
|
...status2 ? { status: status2 } : {},
|
|
2657
2771
|
...currency ? { currency } : {},
|
|
2658
2772
|
...amountCents !== void 0 ? { amountCents } : {},
|
|
@@ -2674,8 +2788,8 @@ function normalizeNetworkCommercialStripeEvent(input, digest) {
|
|
|
2674
2788
|
var identityFields = ["appId", "chapterId", "environment", "dataEnvironment", "runtime"];
|
|
2675
2789
|
function directSource(fact, sources) {
|
|
2676
2790
|
const mode = fact.livemode ? "live" : "test";
|
|
2677
|
-
const supplied = identityFields.filter((
|
|
2678
|
-
const candidates = sources.filter((source) => source.stripeMode === mode && supplied.every((
|
|
2791
|
+
const supplied = identityFields.filter((field2) => fact.identity[field2] !== void 0);
|
|
2792
|
+
const candidates = sources.filter((source) => source.stripeMode === mode && supplied.every((field2) => source[field2] === fact.identity[field2]));
|
|
2679
2793
|
if (supplied.length === identityFields.length) return candidates.length === 1 ? candidates[0] : "wrong";
|
|
2680
2794
|
if (supplied.length > 0 && candidates.length === 0) return "wrong";
|
|
2681
2795
|
return null;
|
|
@@ -2771,11 +2885,11 @@ function projectNetworkCommercialEvents(input, sources) {
|
|
|
2771
2885
|
}
|
|
2772
2886
|
}
|
|
2773
2887
|
const totalsMap = /* @__PURE__ */ new Map();
|
|
2774
|
-
const add = (row2,
|
|
2888
|
+
const add = (row2, field2, amount) => {
|
|
2775
2889
|
if (!row2.currency) return;
|
|
2776
2890
|
const key3 = `${row2.sourceKey}:${row2.currency}`;
|
|
2777
2891
|
const total = totalsMap.get(key3) ?? { sourceKey: row2.sourceKey, currency: row2.currency, paidCents: 0, refundedCents: 0, netCents: 0 };
|
|
2778
|
-
total[
|
|
2892
|
+
total[field2] += amount;
|
|
2779
2893
|
total.netCents = total.paidCents - total.refundedCents;
|
|
2780
2894
|
totalsMap.set(key3, total);
|
|
2781
2895
|
};
|
|
@@ -2783,7 +2897,7 @@ function projectNetworkCommercialEvents(input, sources) {
|
|
|
2783
2897
|
for (const row2 of seatPayments) add(row2, "paidCents", row2.amountReceivedCents ?? 0);
|
|
2784
2898
|
for (const row2 of refunds) add(row2, "refundedCents", row2.amountCents ?? 0);
|
|
2785
2899
|
return {
|
|
2786
|
-
...seatPayments.length ? { seatPayments } : {},
|
|
2900
|
+
...seatPayments.length ? { seatPayments, seats: projectCommercialSeats(seatPayments, subscriptions, refunds) } : {},
|
|
2787
2901
|
customers,
|
|
2788
2902
|
subscriptions,
|
|
2789
2903
|
invoices,
|
|
@@ -3246,7 +3360,7 @@ function findTier(tiers2, tierId) {
|
|
|
3246
3360
|
if (!tierId) return tiers2.length === 1 ? tiers2[0] : null;
|
|
3247
3361
|
return tiers2.find((tier) => tier.id === tierId) ?? null;
|
|
3248
3362
|
}
|
|
3249
|
-
function joinConfigTiers(tiers2, defaults = {}) {
|
|
3363
|
+
function joinConfigTiers(tiers2, defaults = {}, founding = /* @__PURE__ */ new Map()) {
|
|
3250
3364
|
return tiers2.map((tier) => ({
|
|
3251
3365
|
id: tier.id,
|
|
3252
3366
|
name: tier.name,
|
|
@@ -3254,7 +3368,8 @@ function joinConfigTiers(tiers2, defaults = {}) {
|
|
|
3254
3368
|
currency: tier.currency ?? defaults.currency ?? "usd",
|
|
3255
3369
|
interval: tier.interval ?? defaults.interval ?? "year",
|
|
3256
3370
|
blurb: tier.blurb ?? "",
|
|
3257
|
-
free: tierIsFree(tier)
|
|
3371
|
+
free: tierIsFree(tier),
|
|
3372
|
+
...founding.has(tier.id) ? { founding: founding.get(tier.id) } : {}
|
|
3258
3373
|
}));
|
|
3259
3374
|
}
|
|
3260
3375
|
|
|
@@ -3357,8 +3472,8 @@ function createChapterIntegration(chapter, options = {}) {
|
|
|
3357
3472
|
}
|
|
3358
3473
|
|
|
3359
3474
|
// src/membership-policy.ts
|
|
3360
|
-
var fail2 = (
|
|
3361
|
-
throw new TypeError(`invalid membership policy: ${
|
|
3475
|
+
var fail2 = (field2) => {
|
|
3476
|
+
throw new TypeError(`invalid membership policy: ${field2}`);
|
|
3362
3477
|
};
|
|
3363
3478
|
var object = (value) => {
|
|
3364
3479
|
if (!value || typeof value !== "object" || Array.isArray(value)) return fail2("object");
|
|
@@ -3540,23 +3655,63 @@ async function verifyIssuedMembershipOffer(value, expected) {
|
|
|
3540
3655
|
if (!value || typeof value !== "object" || Array.isArray(value)) return reject("invalid_offer_snapshot");
|
|
3541
3656
|
return restore({ ...value, approvedFoundingCount: 0 }, expected);
|
|
3542
3657
|
}
|
|
3543
|
-
async function
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3658
|
+
async function previewMembershipCommercialOffer(db, input) {
|
|
3659
|
+
for (const key3 of ["appId", "chapterId", "environment", "runtime", "skuId"]) {
|
|
3660
|
+
if (typeof input[key3] !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_.:-]{1,159}$/.test(input[key3])) reject("invalid_offer_identity");
|
|
3661
|
+
}
|
|
3662
|
+
if (!Number.isSafeInteger(input.now) || input.now < 0) reject("invalid_offer_window");
|
|
3663
|
+
const state = await authoritativeState(db, input);
|
|
3664
|
+
const { offering, policy } = await currentPolicy(db, input, false, state);
|
|
3665
|
+
const approvedFoundingCount = policy ? state.cohortCounts[policy.cohortLabel] ?? 0 : 0;
|
|
3666
|
+
if (!Number.isSafeInteger(approvedFoundingCount) || Number(approvedFoundingCount) < 0) reject("invalid_cohort_count");
|
|
3667
|
+
const contract = createMembershipCommercialOffer({
|
|
3668
|
+
offering,
|
|
3669
|
+
discountPolicy: policy,
|
|
3670
|
+
approvedFoundingCount: Number(approvedFoundingCount),
|
|
3671
|
+
now: input.now,
|
|
3672
|
+
couponId: await couponId(input, policy?.id)
|
|
3673
|
+
});
|
|
3674
|
+
const discount = contract.billingTerms.discount;
|
|
3675
|
+
return {
|
|
3676
|
+
skuId: input.skuId,
|
|
3677
|
+
offeringVersionId: offering.id,
|
|
3678
|
+
basePriceCents: offering.basePriceCents,
|
|
3679
|
+
currency: offering.currency,
|
|
3680
|
+
interval: offering.interval,
|
|
3681
|
+
founding: contract.founding,
|
|
3682
|
+
...contract.founding && contract.discountPolicy ? { cohortLabel: contract.discountPolicy.cohortLabel } : {},
|
|
3683
|
+
dueTodayCents: contract.dueTodayCents,
|
|
3684
|
+
renewalCents: contract.renewalCents,
|
|
3685
|
+
...discount ? { discountDuration: discount.duration === "once" ? "first_term" : "while_active" } : {},
|
|
3686
|
+
...contract.initialTermEndsAt !== void 0 ? { initialTermEndsAt: contract.initialTermEndsAt } : {}
|
|
3687
|
+
};
|
|
3688
|
+
}
|
|
3689
|
+
async function authoritativeState(db, input) {
|
|
3690
|
+
const state = await load(db, "membershipAuthorityState", `${input.chapterId}:${input.environment}`);
|
|
3547
3691
|
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");
|
|
3548
|
-
|
|
3549
|
-
|
|
3692
|
+
return state;
|
|
3693
|
+
}
|
|
3694
|
+
async function currentPolicy(db, input, restartAware, state) {
|
|
3550
3695
|
const rows3 = await db.query({
|
|
3551
3696
|
membershipOfferingVersion: { $: { where: { chapterId: input.chapterId }, limit: 200 } },
|
|
3552
3697
|
discountPolicyVersion: { $: { where: { chapterId: input.chapterId }, limit: 200 } }
|
|
3553
3698
|
});
|
|
3554
3699
|
if (!Array.isArray(rows3.membershipOfferingVersion) || !Array.isArray(rows3.discountPolicyVersion) || rows3.membershipOfferingVersion.length >= 200 || rows3.discountPolicyVersion.length >= 200) return reject("policy_history_incomplete");
|
|
3555
|
-
const offerings = rows3.membershipOfferingVersion.map((
|
|
3556
|
-
const policies = rows3.discountPolicyVersion.map((
|
|
3700
|
+
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));
|
|
3701
|
+
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);
|
|
3557
3702
|
const offering = offerings.sort((a, b) => b.version - a.version)[0];
|
|
3558
|
-
const
|
|
3703
|
+
const restart = restartAware && input.applicationId ? await load(db, "membershipRestart", (await membershipOfferIdForRequest({ ...input, applicationId: input.applicationId, requestId: "checkout_binding" })).replace(/^offer_/, "membership_")) : void 0;
|
|
3704
|
+
const policy = restart ? void 0 : policies.sort((a, b) => b.version - a.version)[0];
|
|
3559
3705
|
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");
|
|
3706
|
+
return { offering, policy };
|
|
3707
|
+
}
|
|
3708
|
+
async function issueMembershipCommercialOffer(db, input) {
|
|
3709
|
+
validate(input);
|
|
3710
|
+
const stateId = `${input.chapterId}:${input.environment}`;
|
|
3711
|
+
const state = await authoritativeState(db, input);
|
|
3712
|
+
const existing = await load(db, "membershipCheckoutOffer", input.offerId);
|
|
3713
|
+
if (existing) return restore(existing, input);
|
|
3714
|
+
const { offering, policy } = await currentPolicy(db, input, true, state);
|
|
3560
3715
|
const approvedFoundingCount = policy ? state.cohortCounts[policy.cohortLabel] ?? 0 : 0;
|
|
3561
3716
|
if (!Number.isSafeInteger(approvedFoundingCount) || Number(approvedFoundingCount) < 0) reject("invalid_cohort_count");
|
|
3562
3717
|
const contract = createMembershipCommercialOffer({
|
|
@@ -3593,8 +3748,73 @@ async function issueMembershipCommercialOffer(db, input) {
|
|
|
3593
3748
|
return restore(stored, input);
|
|
3594
3749
|
}
|
|
3595
3750
|
|
|
3596
|
-
// src/membership-authority
|
|
3597
|
-
var
|
|
3751
|
+
// src/membership-cancel-authority.ts
|
|
3752
|
+
var import_stripe2 = require("@odla-ai/stripe");
|
|
3753
|
+
|
|
3754
|
+
// src/commercial-evidence-guard.ts
|
|
3755
|
+
async function commercialEvidenceGuard(db, eventId, digest, code = "membership_payment_evidence_quarantined") {
|
|
3756
|
+
const ns = "networkCommercialExceptions", id10 = `network-commercial-exception:${eventId}:collision`;
|
|
3757
|
+
const row2 = (await db.query({ [ns]: { $: { where: { id: id10 }, limit: 1 } } }))[ns]?.[0];
|
|
3758
|
+
if (!row2) return { ns, id: id10, exists: false };
|
|
3759
|
+
if (row2.status !== "resolved" || row2.resolvedDigest !== digest || typeof digest !== "string") {
|
|
3760
|
+
throw Object.assign(new Error(code), { code });
|
|
3761
|
+
}
|
|
3762
|
+
return { ns, id: id10, exists: true, equals: { status: "resolved", resolvedDigest: digest, updatedAt: row2.updatedAt } };
|
|
3763
|
+
}
|
|
3764
|
+
|
|
3765
|
+
// src/membership-authority-state.ts
|
|
3766
|
+
var membershipFail = (code) => {
|
|
3767
|
+
throw Object.assign(new Error(code), { code });
|
|
3768
|
+
};
|
|
3769
|
+
var membershipLoad = async (db, ns, id10) => (await db.query({ [ns]: { $: { where: { id: id10 }, limit: 1 } } }))[ns]?.[0];
|
|
3770
|
+
var membershipScopeKeys = ["appId", "chapterId", "environment", "runtime", "applicationId", "skuId"];
|
|
3771
|
+
var membershipScope = (input) => Object.fromEntries(membershipScopeKeys.map((key3) => [key3, input[key3]]));
|
|
3772
|
+
function requireMembershipScope(row2, input) {
|
|
3773
|
+
if (!row2 || membershipScopeKeys.some((key3) => row2[key3] !== input[key3])) membershipFail("membership_scope_mismatch");
|
|
3774
|
+
}
|
|
3775
|
+
async function loadMembershipAuthorityState(db, input) {
|
|
3776
|
+
const row2 = await membershipLoad(db, "membershipAuthorityState", `${input.chapterId}:${input.environment}`);
|
|
3777
|
+
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");
|
|
3778
|
+
return row2;
|
|
3779
|
+
}
|
|
3780
|
+
var membershipUpdate = (ns, id10, attrs) => ({ t: "update", ns, id: id10, attrs });
|
|
3781
|
+
var membershipClock = (now) => {
|
|
3782
|
+
if (!Number.isSafeInteger(now) || now < 0) membershipFail("invalid_membership_clock");
|
|
3783
|
+
};
|
|
3784
|
+
|
|
3785
|
+
// src/membership-restart-payment.ts
|
|
3786
|
+
async function membershipRestartPayment(db, target, member, now) {
|
|
3787
|
+
const id10 = String(member.id), restart = await membershipLoad(db, "membershipRestart", id10);
|
|
3788
|
+
requireMembershipScope(restart, target);
|
|
3789
|
+
const entitlement = await membershipLoad(db, "networkEntitlement", `${id10}:primary`);
|
|
3790
|
+
requireMembershipScope(entitlement, target);
|
|
3791
|
+
if (entitlement.membershipId !== id10 || entitlement.kind !== "primary") membershipFail("membership_restart_conflict");
|
|
3792
|
+
if (restart.status !== "ready" || member.status !== "restarting" || member.restartId !== restart.restartId || member.memberNumber !== restart.memberNumber || !Number.isSafeInteger(member.journeyApprovedAt)) membershipFail("membership_restart_conflict");
|
|
3793
|
+
return {
|
|
3794
|
+
restartId: String(restart.restartId),
|
|
3795
|
+
entitlementAttrs: { revision: Number(entitlement.revision) + 1, createdAt: entitlement.createdAt },
|
|
3796
|
+
attrs: {
|
|
3797
|
+
memberNumber: member.memberNumber,
|
|
3798
|
+
journeyApprovedAt: member.journeyApprovedAt,
|
|
3799
|
+
founding: member.founding,
|
|
3800
|
+
cohort: member.cohort,
|
|
3801
|
+
...member.personId ? { personId: member.personId } : {},
|
|
3802
|
+
status: "active",
|
|
3803
|
+
revision: Number(member.revision) + 1,
|
|
3804
|
+
createdAt: member.createdAt,
|
|
3805
|
+
graceEndsAt: 0
|
|
3806
|
+
},
|
|
3807
|
+
ops: [
|
|
3808
|
+
membershipUpdate("membershipRestart", id10, { status: "complete" }),
|
|
3809
|
+
{ t: "retract", ns: "networkMembership", id: id10, attrs: ["refundRequestedAt", "cancellationRequestedAt", "discountPolicyVersionId", "discountSnapshot"] }
|
|
3810
|
+
],
|
|
3811
|
+
guards: [
|
|
3812
|
+
{ ns: "networkMembership", id: id10, exists: true, equals: { status: "restarting", revision: member.revision, restartId: restart.restartId } },
|
|
3813
|
+
{ ns: "membershipRestart", id: id10, exists: true, equals: { status: "ready", restartId: restart.restartId } },
|
|
3814
|
+
{ ns: "networkEntitlement", id: String(entitlement.id), exists: true, equals: { revision: entitlement.revision, status: entitlement.status } }
|
|
3815
|
+
]
|
|
3816
|
+
};
|
|
3817
|
+
}
|
|
3598
3818
|
|
|
3599
3819
|
// src/membership-checkout-binding.ts
|
|
3600
3820
|
var keys = ["appId", "chapterId", "environment", "runtime", "applicationId", "skuId", "offerId", "offerDigest", "quoteDigest", "priceId"];
|
|
@@ -3621,10 +3841,11 @@ async function bindMembershipCheckout(db, input, now) {
|
|
|
3621
3841
|
const row2 = await load2(db, "membershipCheckoutOffer", input.offerId);
|
|
3622
3842
|
const offer = await verifyIssuedMembershipOffer(row2, { ...input, now, lifetimeMs: 6e4 });
|
|
3623
3843
|
if (offer.digest !== input.offerDigest || offer.expiresAt <= now || offer.issuedAt > now) return reject2("membership_offer_expired");
|
|
3844
|
+
const restart = await load2(db, "membershipRestart", id10.replace(/^checkout_/, "membership_"));
|
|
3624
3845
|
const receipt = { id: id10, ...Object.fromEntries(keys.map((key3) => [key3, input[key3]])), boundAt: now };
|
|
3625
3846
|
try {
|
|
3626
3847
|
await db.transact([{ t: "update", ns: "membershipOfferCheckout", id: id10, attrs: receipt }], {
|
|
3627
|
-
mutationId: `membership-checkout:${id10}`,
|
|
3848
|
+
mutationId: `membership-checkout:${id10}${restart?.restartId ? `:${restart.restartId}` : ""}`,
|
|
3628
3849
|
guards: [
|
|
3629
3850
|
{ ns: "membershipOfferCheckout", id: id10, exists: false },
|
|
3630
3851
|
{ ns: "membershipCheckoutOffer", id: input.offerId, exists: true, equals: { digest: input.offerDigest } },
|
|
@@ -3639,26 +3860,6 @@ async function bindMembershipCheckout(db, input, now) {
|
|
|
3639
3860
|
return verifyMembershipCheckoutBinding(saved, input);
|
|
3640
3861
|
}
|
|
3641
3862
|
|
|
3642
|
-
// src/membership-authority-state.ts
|
|
3643
|
-
var membershipFail = (code) => {
|
|
3644
|
-
throw Object.assign(new Error(code), { code });
|
|
3645
|
-
};
|
|
3646
|
-
var membershipLoad = async (db, ns, id10) => (await db.query({ [ns]: { $: { where: { id: id10 }, limit: 1 } } }))[ns]?.[0];
|
|
3647
|
-
var membershipScopeKeys = ["appId", "chapterId", "environment", "runtime", "applicationId", "skuId"];
|
|
3648
|
-
var membershipScope = (input) => Object.fromEntries(membershipScopeKeys.map((key3) => [key3, input[key3]]));
|
|
3649
|
-
function requireMembershipScope(row2, input) {
|
|
3650
|
-
if (!row2 || membershipScopeKeys.some((key3) => row2[key3] !== input[key3])) membershipFail("membership_scope_mismatch");
|
|
3651
|
-
}
|
|
3652
|
-
async function loadMembershipAuthorityState(db, input) {
|
|
3653
|
-
const row2 = await membershipLoad(db, "membershipAuthorityState", `${input.chapterId}:${input.environment}`);
|
|
3654
|
-
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");
|
|
3655
|
-
return row2;
|
|
3656
|
-
}
|
|
3657
|
-
var membershipUpdate = (ns, id10, attrs) => ({ t: "update", ns, id: id10, attrs });
|
|
3658
|
-
var membershipClock = (now) => {
|
|
3659
|
-
if (!Number.isSafeInteger(now) || now < 0) membershipFail("invalid_membership_clock");
|
|
3660
|
-
};
|
|
3661
|
-
|
|
3662
3863
|
// src/membership-payment-authority.ts
|
|
3663
3864
|
async function networkMembershipId(input) {
|
|
3664
3865
|
return (await membershipCheckoutBindingId(input)).replace(/^checkout_/, "membership_");
|
|
@@ -3676,9 +3877,7 @@ async function confirmMembershipPayment(db, input, now) {
|
|
|
3676
3877
|
if (binding.offerDigest !== offer.digest) membershipFail("membership_checkout_conflict");
|
|
3677
3878
|
const eventRow = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
|
|
3678
3879
|
if (!eventRow) membershipFail("membership_payment_evidence_pending");
|
|
3679
|
-
|
|
3680
|
-
membershipFail("membership_payment_evidence_quarantined");
|
|
3681
|
-
}
|
|
3880
|
+
const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, eventRow.digest);
|
|
3682
3881
|
const fact = eventRow.fact;
|
|
3683
3882
|
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({
|
|
3684
3883
|
appId: input.appId,
|
|
@@ -3694,8 +3893,9 @@ async function confirmMembershipPayment(db, input, now) {
|
|
|
3694
3893
|
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");
|
|
3695
3894
|
const id10 = await networkMembershipId(input), entitlementId = `${id10}:primary`;
|
|
3696
3895
|
let member = await membershipLoad(db, "networkMembership", id10);
|
|
3697
|
-
if (!member) {
|
|
3896
|
+
if (!member || member.status === "restarting") {
|
|
3698
3897
|
const offering = offer.contract.offering;
|
|
3898
|
+
const restart = member ? await membershipRestartPayment(db, input, member, now) : null;
|
|
3699
3899
|
const attrs = {
|
|
3700
3900
|
id: id10,
|
|
3701
3901
|
membershipId: id10,
|
|
@@ -3727,7 +3927,8 @@ async function confirmMembershipPayment(db, input, now) {
|
|
|
3727
3927
|
refundedCents: 0,
|
|
3728
3928
|
revision: 1,
|
|
3729
3929
|
createdAt: now,
|
|
3730
|
-
updatedAt: now
|
|
3930
|
+
updatedAt: now,
|
|
3931
|
+
...restart?.attrs
|
|
3731
3932
|
};
|
|
3732
3933
|
try {
|
|
3733
3934
|
await db.transact([
|
|
@@ -3737,21 +3938,23 @@ async function confirmMembershipPayment(db, input, now) {
|
|
|
3737
3938
|
...membershipScope(input),
|
|
3738
3939
|
membershipId: id10,
|
|
3739
3940
|
kind: "primary",
|
|
3740
|
-
status: "pending_approval",
|
|
3941
|
+
status: restart?.attrs.status === "active" ? "active" : "pending_approval",
|
|
3741
3942
|
termStartedAt: paidAt,
|
|
3742
3943
|
termEndsAt: endsAt,
|
|
3743
3944
|
offeringSnapshot: offering,
|
|
3744
3945
|
sourceEventId: input.eventId,
|
|
3745
3946
|
revision: 1,
|
|
3746
3947
|
createdAt: now,
|
|
3747
|
-
updatedAt: now
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3948
|
+
updatedAt: now,
|
|
3949
|
+
...restart?.entitlementAttrs
|
|
3950
|
+
}),
|
|
3951
|
+
...restart?.ops ?? []
|
|
3952
|
+
], { mutationId: `membership-paid:${id10}${restart ? `:${restart.restartId}` : ""}`, guards: [
|
|
3953
|
+
...restart?.guards ?? [{ ns: "networkMembership", id: id10, exists: false }],
|
|
3751
3954
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
|
|
3752
3955
|
{ ns: "membershipOfferCheckout", id: String(binding.id), exists: true, equals: { offerDigest: offer.digest } },
|
|
3753
3956
|
{ ns: "networkCommercialEvents", id: String(eventRow.id), exists: true, equals: { digest: eventRow.digest } },
|
|
3754
|
-
|
|
3957
|
+
evidenceGuard
|
|
3755
3958
|
] });
|
|
3756
3959
|
} catch (error) {
|
|
3757
3960
|
if (error.code !== "transact_guard_failed") throw error;
|
|
@@ -3770,6 +3973,57 @@ async function confirmMembershipPayment(db, input, now) {
|
|
|
3770
3973
|
};
|
|
3771
3974
|
}
|
|
3772
3975
|
|
|
3976
|
+
// src/membership-cancel-authority.ts
|
|
3977
|
+
async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
|
|
3978
|
+
membershipClock(now);
|
|
3979
|
+
const id10 = await networkMembershipId(target);
|
|
3980
|
+
const state = await loadMembershipAuthorityState(db, target);
|
|
3981
|
+
const member = await membershipLoad(db, "networkMembership", id10);
|
|
3982
|
+
requireMembershipScope(member, target);
|
|
3983
|
+
if (!["active", "cancel_at_term_end", "payment_grace", "paid_pending_approval", "lapsed"].includes(String(member.status))) {
|
|
3984
|
+
membershipFail("membership_cancellation_unavailable");
|
|
3985
|
+
}
|
|
3986
|
+
const path = `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`;
|
|
3987
|
+
const read2 = await (0, import_stripe2.stripeRequest)({ secretKey: stripeSecret }, "GET", path);
|
|
3988
|
+
const sub = read2.body;
|
|
3989
|
+
if (!read2.ok || sub.id !== member.providerSubscriptionId || sub.customer !== member.providerCustomerId || sub.livemode !== (target.environment === "prod") || !Object.entries({
|
|
3990
|
+
...stripePartitionMetadata(target),
|
|
3991
|
+
applicationId: target.applicationId
|
|
3992
|
+
}).every(([key3, value]) => sub.metadata?.[key3] === value)) membershipFail("membership_cancellation_provider_mismatch");
|
|
3993
|
+
if (sub.status !== "canceled" && sub.cancel_at_period_end !== true) {
|
|
3994
|
+
const write = await (0, import_stripe2.stripeRequest)(
|
|
3995
|
+
{ secretKey: stripeSecret },
|
|
3996
|
+
"POST",
|
|
3997
|
+
path,
|
|
3998
|
+
{ cancel_at_period_end: true, proration_behavior: "none" },
|
|
3999
|
+
{ idempotencyKey: `membership-cancel-at-term:${id10}:${member.providerSubscriptionId}` }
|
|
4000
|
+
);
|
|
4001
|
+
if (!write.ok || write.body.id !== member.providerSubscriptionId || write.body.cancel_at_period_end !== true) {
|
|
4002
|
+
membershipFail("membership_cancellation_pending");
|
|
4003
|
+
}
|
|
4004
|
+
}
|
|
4005
|
+
const status2 = Number(member.termEndsAt) <= now ? "lapsed" : member.journeyApprovedAt ? "cancel_at_term_end" : "paid_pending_approval";
|
|
4006
|
+
if (member.status !== status2 || member.autoRenew !== false || Number(member.graceEndsAt ?? 0) !== 0) {
|
|
4007
|
+
await db.transact([membershipUpdate("networkMembership", id10, {
|
|
4008
|
+
status: status2,
|
|
4009
|
+
autoRenew: false,
|
|
4010
|
+
graceEndsAt: 0,
|
|
4011
|
+
cancellationRequestedAt: now,
|
|
4012
|
+
revision: Number(member.revision) + 1,
|
|
4013
|
+
updatedAt: now
|
|
4014
|
+
})], {
|
|
4015
|
+
guards: [
|
|
4016
|
+
{ ns: "networkMembership", id: id10, exists: true, equals: { revision: member.revision } },
|
|
4017
|
+
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { revision: state.revision, mode: "authoritative" } }
|
|
4018
|
+
]
|
|
4019
|
+
});
|
|
4020
|
+
}
|
|
4021
|
+
return { membershipId: id10, status: status2, termEndsAt: Number(member.termEndsAt), graceEndsAt: 0, autoRenew: false };
|
|
4022
|
+
}
|
|
4023
|
+
|
|
4024
|
+
// src/membership-authority-handler.ts
|
|
4025
|
+
var import_db = require("@odla-ai/db");
|
|
4026
|
+
|
|
3773
4027
|
// src/membership-seat-activation.ts
|
|
3774
4028
|
async function namedSeatPrimaryActivation(db, member, now) {
|
|
3775
4029
|
if (!member.namedSeatId) return { ops: [], guards: [] };
|
|
@@ -3862,7 +4116,7 @@ async function approveNetworkMembership(db, input, now) {
|
|
|
3862
4116
|
}
|
|
3863
4117
|
|
|
3864
4118
|
// src/payments-refund.ts
|
|
3865
|
-
var
|
|
4119
|
+
var import_stripe3 = require("@odla-ai/stripe");
|
|
3866
4120
|
var row = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
3867
4121
|
var id7 = (value) => typeof value === "string" ? value : String(row(value).id ?? "");
|
|
3868
4122
|
var providerId2 = (value, prefix) => new RegExp(`^${prefix}_[A-Za-z0-9]+$`).test(value);
|
|
@@ -3872,7 +4126,7 @@ function fail3(code = "refund_target_mismatch") {
|
|
|
3872
4126
|
async function refundSubscriptionPayment(secretKey, input) {
|
|
3873
4127
|
if (!providerId2(input.customerId, "cus") || !providerId2(input.subscriptionId, "sub")) fail3();
|
|
3874
4128
|
const call = async (method, path, params, key3) => {
|
|
3875
|
-
const result = await (0,
|
|
4129
|
+
const result = await (0, import_stripe3.stripeRequest)({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
|
|
3876
4130
|
if (!result.ok) fail3("stripe_error");
|
|
3877
4131
|
if (!result.body || typeof result.body !== "object" || Array.isArray(result.body)) fail3("stripe_invalid_response");
|
|
3878
4132
|
return result.body;
|
|
@@ -4043,12 +4297,13 @@ async function refundNetworkMembership(db, input, secretKey, now) {
|
|
|
4043
4297
|
}
|
|
4044
4298
|
|
|
4045
4299
|
// src/membership-provider-lifecycle.ts
|
|
4046
|
-
var
|
|
4300
|
+
var import_stripe4 = require("@odla-ai/stripe");
|
|
4047
4301
|
|
|
4048
4302
|
// src/membership-renewal-authority.ts
|
|
4049
4303
|
async function reconcileMembershipRenewal(db, input, now) {
|
|
4050
4304
|
membershipClock(now);
|
|
4051
4305
|
const id10 = await networkMembershipId(input), eventRow = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
|
|
4306
|
+
const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, eventRow?.digest, "membership_renewal_evidence_pending");
|
|
4052
4307
|
const fact = eventRow?.fact;
|
|
4053
4308
|
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({
|
|
4054
4309
|
appId: input.appId,
|
|
@@ -4056,7 +4311,7 @@ async function reconcileMembershipRenewal(db, input, now) {
|
|
|
4056
4311
|
environment: input.environment,
|
|
4057
4312
|
dataEnvironment: input.environment,
|
|
4058
4313
|
runtime: input.runtime
|
|
4059
|
-
}).some(([key3, value]) => fact.identity[key3] !== value)
|
|
4314
|
+
}).some(([key3, value]) => fact.identity[key3] !== value)) {
|
|
4060
4315
|
return membershipFail("membership_renewal_evidence_pending");
|
|
4061
4316
|
}
|
|
4062
4317
|
for (let attempt = 0; attempt < 8; attempt++) {
|
|
@@ -4070,7 +4325,7 @@ async function reconcileMembershipRenewal(db, input, now) {
|
|
|
4070
4325
|
autoRenew: member.autoRenew === true
|
|
4071
4326
|
});
|
|
4072
4327
|
if (fact.subscriptionId !== member.providerSubscriptionId || fact.customerId !== member.providerCustomerId || fact.currency !== member.currency) membershipFail("membership_renewal_scope_mismatch");
|
|
4073
|
-
if (["refunded", "refund_pending", "canceled", "declined"].includes(String(member.status))) return receipt();
|
|
4328
|
+
if (["refunded", "refund_pending", "canceled", "declined", "restarting", "superseded"].includes(String(member.status))) return receipt();
|
|
4074
4329
|
const lines = fact.recurringInvoiceLines;
|
|
4075
4330
|
if (!lines?.length || lines.length > 2) membershipFail("membership_renewal_lines_missing");
|
|
4076
4331
|
const start = lines[0].startsAt, end = lines[0].endsAt;
|
|
@@ -4079,7 +4334,7 @@ async function reconcileMembershipRenewal(db, input, now) {
|
|
|
4079
4334
|
if (start !== member.termEndsAt || start > now + 3e4) membershipFail("membership_renewal_period_gap");
|
|
4080
4335
|
const seat = member.namedSeatId ? await membershipLoad(db, "networkSeat", String(member.namedSeatId)) : void 0;
|
|
4081
4336
|
if (seat) requireMembershipScope(seat, input);
|
|
4082
|
-
const billedSeat = seat && seat.providerSubscriptionItemId && !["refunded", "refund_pending"].includes(String(seat.status)) ? seat : void 0;
|
|
4337
|
+
const billedSeat = seat && seat.providerSubscriptionId === member.providerSubscriptionId && seat.providerSubscriptionItemId && !["refunded", "refund_pending"].includes(String(seat.status)) ? seat : void 0;
|
|
4083
4338
|
const prices = [String(member.providerPriceId), ...billedSeat ? [String(billedSeat.priceId)] : []].sort();
|
|
4084
4339
|
const amount = Number(member.renewalAmountCents) + Number(billedSeat?.amountCents ?? 0);
|
|
4085
4340
|
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");
|
|
@@ -4112,7 +4367,7 @@ async function reconcileMembershipRenewal(db, input, now) {
|
|
|
4112
4367
|
{ ns: "networkEntitlement", id: `${id10}:primary`, exists: true, equals: { revision: entitlement.revision } },
|
|
4113
4368
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { revision: state.revision, mode: "authoritative" } },
|
|
4114
4369
|
{ ns: "networkCommercialEvents", id: String(eventRow.id), exists: true, equals: { digest: eventRow.digest } },
|
|
4115
|
-
|
|
4370
|
+
evidenceGuard
|
|
4116
4371
|
];
|
|
4117
4372
|
if (seat) guards.push({ ns: "networkSeat", id: String(seat.id), exists: true, equals: { revision: seat.revision } });
|
|
4118
4373
|
if (billedSeat?.entitlementId) {
|
|
@@ -4158,14 +4413,14 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
4158
4413
|
const row2 = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
|
|
4159
4414
|
const fact = row2?.fact;
|
|
4160
4415
|
if (fact?.objectKind === "invoice") return reconcileMembershipRenewal(db, input, now);
|
|
4161
|
-
const
|
|
4416
|
+
const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, row2?.digest, "membership_billing_evidence_pending");
|
|
4162
4417
|
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({
|
|
4163
4418
|
appId: input.appId,
|
|
4164
4419
|
chapterId: input.chapterId,
|
|
4165
4420
|
environment: input.environment,
|
|
4166
4421
|
dataEnvironment: input.environment,
|
|
4167
4422
|
runtime: input.runtime
|
|
4168
|
-
}).some(([key3, value]) => fact.identity[key3] !== value)
|
|
4423
|
+
}).some(([key3, value]) => fact.identity[key3] !== value)) membershipFail("membership_billing_evidence_pending");
|
|
4169
4424
|
const sk = await getVaultSecret(db, stripeSecretName);
|
|
4170
4425
|
if (!sk) membershipFail("membership_billing_provider_unavailable");
|
|
4171
4426
|
const id10 = await networkMembershipId(input);
|
|
@@ -4179,6 +4434,7 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
4179
4434
|
graceEndsAt: Number(value.graceEndsAt ?? 0),
|
|
4180
4435
|
autoRenew: value.autoRenew === true
|
|
4181
4436
|
});
|
|
4437
|
+
if (["restarting"].includes(String(member.status))) return receipt();
|
|
4182
4438
|
if (fact.customerId !== member.providerCustomerId) membershipFail("membership_billing_scope_mismatch");
|
|
4183
4439
|
if (fact.eventType === "charge.refunded") {
|
|
4184
4440
|
if (!fact.chargeId || fact.seatId || fact.amountCents !== member.amountCents || fact.currency !== member.currency) membershipFail("membership_refund_evidence_mismatch");
|
|
@@ -4191,15 +4447,15 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
4191
4447
|
alreadyRefundedChargeId: fact.chargeId,
|
|
4192
4448
|
cancelSubscription: false
|
|
4193
4449
|
});
|
|
4194
|
-
|
|
4450
|
+
await commercialEvidenceGuard(db, input.eventId, row2?.digest, "membership_billing_evidence_pending");
|
|
4195
4451
|
await refundNetworkMembership(db, input, sk, now);
|
|
4196
4452
|
const saved2 = await membershipLoad(db, "networkMembership", id10);
|
|
4197
4453
|
requireMembershipScope(saved2, input);
|
|
4198
4454
|
return receipt(saved2);
|
|
4199
4455
|
}
|
|
4200
4456
|
if (fact.subscriptionId !== member.providerSubscriptionId || fact.objectKind !== "subscription") membershipFail("membership_billing_scope_mismatch");
|
|
4201
|
-
if (["refund_pending", "refunded", "declined", "canceled"].includes(String(member.status))) return receipt();
|
|
4202
|
-
const result = await (0,
|
|
4457
|
+
if (["refund_pending", "refunded", "declined", "canceled", "restarting"].includes(String(member.status))) return receipt();
|
|
4458
|
+
const result = await (0, import_stripe4.stripeRequest)({ secretKey: sk }, "GET", `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`);
|
|
4203
4459
|
const sub = result.body;
|
|
4204
4460
|
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)) {
|
|
4205
4461
|
membershipFail("membership_billing_provider_mismatch");
|
|
@@ -4223,7 +4479,7 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
4223
4479
|
{ ns: "networkEntitlement", id: String(entitlement.id), exists: true, equals: { revision: entitlement.revision } },
|
|
4224
4480
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
|
|
4225
4481
|
{ ns: "networkCommercialEvents", id: String(row2.id), exists: true, equals: { digest: fact.digest } },
|
|
4226
|
-
|
|
4482
|
+
evidenceGuard
|
|
4227
4483
|
] });
|
|
4228
4484
|
} catch (error) {
|
|
4229
4485
|
if (error.code !== "transact_guard_failed") throw error;
|
|
@@ -4236,6 +4492,120 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
4236
4492
|
return membershipFail("membership_billing_retry_required");
|
|
4237
4493
|
}
|
|
4238
4494
|
|
|
4495
|
+
// src/membership-restart-seat.ts
|
|
4496
|
+
async function expireRestartedMembershipSeat(db, target, member, now) {
|
|
4497
|
+
const ops = [], guards = [];
|
|
4498
|
+
if (!member.namedSeatId) return { ops, guards };
|
|
4499
|
+
const seat = await membershipLoad(db, "networkSeat", String(member.namedSeatId));
|
|
4500
|
+
requireMembershipScope(seat, target);
|
|
4501
|
+
if (!["refunded", "refund_pending", "expired"].includes(String(seat.status))) {
|
|
4502
|
+
ops.push(membershipUpdate("networkSeat", String(seat.id), { status: "expired", revision: Number(seat.revision) + 1, updatedAt: now }));
|
|
4503
|
+
guards.push({ ns: "networkSeat", id: String(seat.id), exists: true, equals: { revision: seat.revision } });
|
|
4504
|
+
}
|
|
4505
|
+
if (seat.entitlementId) {
|
|
4506
|
+
const entitlement = await membershipLoad(db, "networkEntitlement", String(seat.entitlementId));
|
|
4507
|
+
requireMembershipScope(entitlement, target);
|
|
4508
|
+
ops.push(membershipUpdate("networkEntitlement", String(entitlement.id), { status: "closed", revision: Number(entitlement.revision) + 1, updatedAt: now }));
|
|
4509
|
+
guards.push({ ns: "networkEntitlement", id: String(entitlement.id), exists: true, equals: { revision: entitlement.revision } });
|
|
4510
|
+
}
|
|
4511
|
+
return { ops, guards };
|
|
4512
|
+
}
|
|
4513
|
+
|
|
4514
|
+
// src/membership-restart-authority.ts
|
|
4515
|
+
var import_stripe5 = require("@odla-ai/stripe");
|
|
4516
|
+
|
|
4517
|
+
// src/stripe-event-digest.ts
|
|
4518
|
+
async function stripePayloadDigest(value) {
|
|
4519
|
+
const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
|
|
4520
|
+
return [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
4521
|
+
}
|
|
4522
|
+
|
|
4523
|
+
// src/membership-restart-authority.ts
|
|
4524
|
+
function membershipCanRestart(member, now) {
|
|
4525
|
+
const end = member.status === "payment_grace" ? Number(member.graceEndsAt) : Number(member.termEndsAt);
|
|
4526
|
+
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);
|
|
4527
|
+
}
|
|
4528
|
+
async function stopPreviousSubscription(member, target, secret) {
|
|
4529
|
+
const path = `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`;
|
|
4530
|
+
const result = await (0, import_stripe5.stripeRequest)({ secretKey: secret }, "GET", path);
|
|
4531
|
+
const sub = result.body;
|
|
4532
|
+
if (!result.ok || sub.id !== member.providerSubscriptionId || sub.customer !== member.providerCustomerId || sub.livemode !== (target.environment === "prod") || !Object.entries({
|
|
4533
|
+
...stripePartitionMetadata(target),
|
|
4534
|
+
applicationId: target.applicationId
|
|
4535
|
+
}).every(([key3, value]) => sub.metadata?.[key3] === value)) membershipFail("membership_restart_provider_mismatch");
|
|
4536
|
+
if (sub.status === "canceled") return;
|
|
4537
|
+
const canceled = await (0, import_stripe5.stripeRequest)(
|
|
4538
|
+
{ secretKey: secret },
|
|
4539
|
+
"DELETE",
|
|
4540
|
+
path,
|
|
4541
|
+
{ invoice_now: false, prorate: false },
|
|
4542
|
+
{ idempotencyKey: `membership-restart-cancel:${member.providerSubscriptionId}` }
|
|
4543
|
+
);
|
|
4544
|
+
if (!canceled.ok || canceled.body.id !== member.providerSubscriptionId || canceled.body.status !== "canceled") membershipFail("membership_restart_cancellation_pending");
|
|
4545
|
+
}
|
|
4546
|
+
async function prepareMembershipRestart(db, input, now, stripeSecret) {
|
|
4547
|
+
membershipClock(now);
|
|
4548
|
+
if (!/^sub_[A-Za-z0-9_]+$/.test(input.previousSubscriptionId)) membershipFail("membership_restart_conflict");
|
|
4549
|
+
const id10 = await networkMembershipId(input), bindingId = await membershipCheckoutBindingId(input);
|
|
4550
|
+
const restartId = `restart_${await stripePayloadDigest(JSON.stringify([id10, input.previousSubscriptionId]))}`;
|
|
4551
|
+
const state = await loadMembershipAuthorityState(db, input);
|
|
4552
|
+
const previous = await membershipLoad(db, "networkMembership", id10);
|
|
4553
|
+
requireMembershipScope(previous, input);
|
|
4554
|
+
const receipt = {
|
|
4555
|
+
applicationId: input.applicationId,
|
|
4556
|
+
membershipId: id10,
|
|
4557
|
+
restartId,
|
|
4558
|
+
memberNumber: Number(previous.memberNumber),
|
|
4559
|
+
status: "ready"
|
|
4560
|
+
};
|
|
4561
|
+
const existing = await membershipLoad(db, "membershipRestart", id10);
|
|
4562
|
+
if (existing?.restartId === restartId && existing.status === "ready") {
|
|
4563
|
+
requireMembershipScope(existing, input);
|
|
4564
|
+
return receipt;
|
|
4565
|
+
}
|
|
4566
|
+
if (previous.providerSubscriptionId !== input.previousSubscriptionId) membershipFail("membership_restart_conflict");
|
|
4567
|
+
if (previous.status !== "restarting") {
|
|
4568
|
+
if (!membershipCanRestart(previous, now)) membershipFail("membership_restart_not_eligible");
|
|
4569
|
+
const binding2 = await membershipLoad(db, "membershipOfferCheckout", bindingId);
|
|
4570
|
+
requireMembershipScope(binding2, input);
|
|
4571
|
+
const seats = await expireRestartedMembershipSeat(db, input, previous, now);
|
|
4572
|
+
await db.transact([
|
|
4573
|
+
...seats.ops,
|
|
4574
|
+
membershipUpdate("membershipPurchaseHistory", restartId, {
|
|
4575
|
+
id: restartId,
|
|
4576
|
+
...membershipScope(input),
|
|
4577
|
+
membershipId: id10,
|
|
4578
|
+
providerSubscriptionId: input.previousSubscriptionId,
|
|
4579
|
+
memberSnapshot: previous,
|
|
4580
|
+
bindingSnapshot: binding2,
|
|
4581
|
+
archivedAt: now
|
|
4582
|
+
}),
|
|
4583
|
+
membershipUpdate("networkMembership", id10, { status: "restarting", restartId, revision: Number(previous.revision) + 1, updatedAt: now })
|
|
4584
|
+
], { mutationId: `membership-restart-reserve:${restartId}`, guards: [
|
|
4585
|
+
...seats.guards,
|
|
4586
|
+
{ ns: "networkMembership", id: id10, exists: true, equals: { revision: previous.revision, status: previous.status } },
|
|
4587
|
+
{ ns: "membershipPurchaseHistory", id: restartId, exists: false },
|
|
4588
|
+
{ ns: "membershipOfferCheckout", id: bindingId, exists: true, equals: { offerDigest: binding2.offerDigest } },
|
|
4589
|
+
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
|
|
4590
|
+
] });
|
|
4591
|
+
} else if (previous.restartId !== restartId) membershipFail("membership_restart_conflict");
|
|
4592
|
+
const reserved = await membershipLoad(db, "networkMembership", id10);
|
|
4593
|
+
requireMembershipScope(reserved, input);
|
|
4594
|
+
if (reserved.status !== "restarting" || reserved.restartId !== restartId) membershipFail("membership_restart_conflict");
|
|
4595
|
+
await stopPreviousSubscription(reserved, input, stripeSecret);
|
|
4596
|
+
const binding = await membershipLoad(db, "membershipOfferCheckout", bindingId);
|
|
4597
|
+
requireMembershipScope(binding, input);
|
|
4598
|
+
await db.transact([
|
|
4599
|
+
membershipUpdate("membershipRestart", id10, { id: id10, ...membershipScope(input), ...receipt, previousSubscriptionId: input.previousSubscriptionId, createdAt: now }),
|
|
4600
|
+
{ t: "delete", ns: "membershipOfferCheckout", id: bindingId }
|
|
4601
|
+
], { mutationId: `membership-restart-ready:${restartId}`, guards: [
|
|
4602
|
+
{ ns: "membershipRestart", id: id10, exists: Boolean(existing), ...existing ? { equals: { restartId: existing.restartId, status: existing.status } } : {} },
|
|
4603
|
+
{ ns: "networkMembership", id: id10, exists: true, equals: { revision: reserved.revision, status: "restarting", restartId } },
|
|
4604
|
+
{ ns: "membershipOfferCheckout", id: bindingId, exists: true, equals: { offerDigest: binding.offerDigest } }
|
|
4605
|
+
] });
|
|
4606
|
+
return receipt;
|
|
4607
|
+
}
|
|
4608
|
+
|
|
4239
4609
|
// src/membership-access-authority.ts
|
|
4240
4610
|
async function readMembershipAccess(db, input, now) {
|
|
4241
4611
|
membershipClock(now);
|
|
@@ -4252,7 +4622,8 @@ async function readMembershipAccess(db, input, now) {
|
|
|
4252
4622
|
allowed: allowed2,
|
|
4253
4623
|
status: String(member.status),
|
|
4254
4624
|
accessEndsAt: Number.isSafeInteger(endsAt) ? endsAt : 0,
|
|
4255
|
-
revision: Number(member.revision)
|
|
4625
|
+
revision: Number(member.revision),
|
|
4626
|
+
restartEligible: !allowed2 && membershipCanRestart(member, now)
|
|
4256
4627
|
};
|
|
4257
4628
|
}
|
|
4258
4629
|
async function verifyMembershipAccessReceipt(value, input, now) {
|
|
@@ -4260,7 +4631,7 @@ async function verifyMembershipAccessReceipt(value, input, now) {
|
|
|
4260
4631
|
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)) {
|
|
4261
4632
|
throw new Error("invalid network access receipt");
|
|
4262
4633
|
}
|
|
4263
|
-
return { membershipId: row2.membershipId, allowed: row2.allowed, status: row2.status, accessEndsAt: row2.accessEndsAt, revision: row2.revision };
|
|
4634
|
+
return { membershipId: row2.membershipId, allowed: row2.allowed, status: row2.status, accessEndsAt: row2.accessEndsAt, revision: row2.revision, ...row2.restartEligible === true && !row2.allowed ? { restartEligible: true } : {} };
|
|
4264
4635
|
}
|
|
4265
4636
|
|
|
4266
4637
|
// src/membership-seat-reservation.ts
|
|
@@ -4341,16 +4712,16 @@ async function reserveNamedMembershipSeat(db, input, now) {
|
|
|
4341
4712
|
}
|
|
4342
4713
|
|
|
4343
4714
|
// src/membership-seat-stripe.ts
|
|
4344
|
-
var
|
|
4715
|
+
var import_stripe7 = require("@odla-ai/stripe");
|
|
4345
4716
|
|
|
4346
4717
|
// src/subscription-provider-contract.ts
|
|
4347
|
-
var
|
|
4718
|
+
var import_stripe6 = require("@odla-ai/stripe");
|
|
4348
4719
|
function subscriptionContractMismatch() {
|
|
4349
4720
|
throw Object.assign(new Error("Stripe subscription does not match the checkout contract"), { code: "stripe_contract_mismatch" });
|
|
4350
4721
|
}
|
|
4351
4722
|
async function verifySubscriptionPrice(secretKey, input, resume = false) {
|
|
4352
4723
|
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();
|
|
4353
|
-
const result = await (0,
|
|
4724
|
+
const result = await (0, import_stripe6.stripeRequest)({ secretKey }, "GET", `/v1/prices/${encodeURIComponent(input.priceId)}`);
|
|
4354
4725
|
const price = result.body;
|
|
4355
4726
|
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();
|
|
4356
4727
|
return typeof price.product === "string" ? price.product : void 0;
|
|
@@ -4368,7 +4739,7 @@ var metadataFor = (seat) => ({
|
|
|
4368
4739
|
});
|
|
4369
4740
|
var matches = (value, expected) => value && Object.entries(expected).every(([key3, item]) => value[key3] === item);
|
|
4370
4741
|
async function read(sk, path, params) {
|
|
4371
|
-
const response = await (0,
|
|
4742
|
+
const response = await (0, import_stripe7.stripeRequest)({ secretKey: sk }, "GET", path, params);
|
|
4372
4743
|
if (!response.ok) return reject3();
|
|
4373
4744
|
return response.body;
|
|
4374
4745
|
}
|
|
@@ -4405,11 +4776,11 @@ async function createNamedSeatStripePayment(sk, seat, now) {
|
|
|
4405
4776
|
}
|
|
4406
4777
|
await subscriptionForSeat(sk, seat);
|
|
4407
4778
|
const metadata = metadataFor(seat);
|
|
4408
|
-
const result = seat.providerPaymentIntentId ? await (0,
|
|
4779
|
+
const result = seat.providerPaymentIntentId ? await (0, import_stripe7.stripeRequest)(
|
|
4409
4780
|
{ secretKey: sk },
|
|
4410
4781
|
"GET",
|
|
4411
4782
|
`/v1/payment_intents/${encodeURIComponent(seat.providerPaymentIntentId)}`
|
|
4412
|
-
) : await (0,
|
|
4783
|
+
) : await (0, import_stripe7.stripeRequest)({ secretKey: sk }, "POST", "/v1/payment_intents", {
|
|
4413
4784
|
amount: seat.amountCents,
|
|
4414
4785
|
currency: seat.currency,
|
|
4415
4786
|
customer: seat.providerCustomerId,
|
|
@@ -4427,7 +4798,7 @@ async function attachNamedSeatStripeRenewal(sk, seat) {
|
|
|
4427
4798
|
const sub = await subscriptionForSeat(sk, seat);
|
|
4428
4799
|
const existing = sub.items.data.find((item2) => item2.metadata?.seatId === seat.id);
|
|
4429
4800
|
if (existing) return String(existing.id);
|
|
4430
|
-
const result = await (0,
|
|
4801
|
+
const result = await (0, import_stripe7.stripeRequest)({ secretKey: sk }, "POST", "/v1/subscription_items", {
|
|
4431
4802
|
subscription: seat.providerSubscriptionId,
|
|
4432
4803
|
price: seat.priceId,
|
|
4433
4804
|
quantity: 1,
|
|
@@ -4505,8 +4876,7 @@ async function confirmNamedMembershipSeatPayment(db, input, now) {
|
|
|
4505
4876
|
requireMembershipScope(member, input);
|
|
4506
4877
|
const row2 = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
|
|
4507
4878
|
if (!row2 || !seat.providerPaymentIntentId) membershipFail("named_seat_payment_evidence_pending");
|
|
4508
|
-
const
|
|
4509
|
-
if (await membershipLoad(db, "networkCommercialExceptions", collisionId)) membershipFail("named_seat_payment_evidence_quarantined");
|
|
4879
|
+
const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, row2.digest, "named_seat_payment_evidence_quarantined");
|
|
4510
4880
|
const fact = row2.fact;
|
|
4511
4881
|
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({
|
|
4512
4882
|
appId: input.appId,
|
|
@@ -4528,7 +4898,7 @@ async function confirmNamedMembershipSeatPayment(db, input, now) {
|
|
|
4528
4898
|
membershipUpdate("networkSeat", input.seatId, {
|
|
4529
4899
|
status: status2,
|
|
4530
4900
|
purchasedAt: paidAt,
|
|
4531
|
-
acceptanceEndsAt:
|
|
4901
|
+
acceptanceEndsAt: Number(member.termEndsAt),
|
|
4532
4902
|
termStartedAt: paidAt,
|
|
4533
4903
|
termEndsAt: member.termEndsAt,
|
|
4534
4904
|
paymentEventId: input.eventId,
|
|
@@ -4553,7 +4923,7 @@ async function confirmNamedMembershipSeatPayment(db, input, now) {
|
|
|
4553
4923
|
{ ns: "networkMembership", id: String(member.id), exists: true, equals: { revision: member.revision } },
|
|
4554
4924
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
|
|
4555
4925
|
{ ns: "networkCommercialEvents", id: String(row2.id), exists: true, equals: { digest: fact.digest } },
|
|
4556
|
-
|
|
4926
|
+
evidenceGuard
|
|
4557
4927
|
] });
|
|
4558
4928
|
} catch (error) {
|
|
4559
4929
|
if (error.code !== "transact_guard_failed") throw error;
|
|
@@ -4576,7 +4946,7 @@ async function completeNamedSeatRenewal(db, seatId, secretKey, now) {
|
|
|
4576
4946
|
if (seat.status !== "renewal_pending" || !seat.purchasedAt) membershipFail("named_seat_renewal_not_ready");
|
|
4577
4947
|
const member = await membershipLoad(db, "networkMembership", target.primaryMembershipId);
|
|
4578
4948
|
requireMembershipScope(member, target);
|
|
4579
|
-
if (now >= Number(
|
|
4949
|
+
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");
|
|
4580
4950
|
const itemId = await attachNamedSeatStripeRenewal(secretKey, target);
|
|
4581
4951
|
const entitlementId = `${seatId}:entitlement`, effectId = `${seatId}:attach-renewal`;
|
|
4582
4952
|
try {
|
|
@@ -4651,7 +5021,7 @@ async function recordRecipient(db, input, now, approve) {
|
|
|
4651
5021
|
requireMembershipScope(entitlement, input);
|
|
4652
5022
|
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");
|
|
4653
5023
|
if (seat.acceptedAt === void 0) {
|
|
4654
|
-
if (approve ||
|
|
5024
|
+
if (approve || normalizeChapterEmail(input.verifiedRecipientEmail) !== seat.recipientEmailNormalized) {
|
|
4655
5025
|
membershipFail("named_seat_recipient_verification_required");
|
|
4656
5026
|
}
|
|
4657
5027
|
} else if (seat.recipientUserId !== input.recipientUserId || seat.recipientApplicationId !== input.recipientApplicationId) {
|
|
@@ -4702,14 +5072,14 @@ async function recordRecipient(db, input, now, approve) {
|
|
|
4702
5072
|
}
|
|
4703
5073
|
|
|
4704
5074
|
// src/membership-seat-refund-stripe.ts
|
|
4705
|
-
var
|
|
5075
|
+
var import_stripe8 = require("@odla-ai/stripe");
|
|
4706
5076
|
var fail4 = (code = "named_seat_refund_mismatch") => {
|
|
4707
5077
|
throw Object.assign(new Error(code), { code });
|
|
4708
5078
|
};
|
|
4709
5079
|
async function refundNamedSeatStripePayment(secretKey, seat) {
|
|
4710
5080
|
if (!seat.providerPaymentIntentId || seat.status !== "refund_pending") fail4();
|
|
4711
5081
|
const call = async (method, path, params, idempotencyKey) => {
|
|
4712
|
-
const result = await (0,
|
|
5082
|
+
const result = await (0, import_stripe8.stripeRequest)({ secretKey }, method, path, params, idempotencyKey ? { idempotencyKey } : {});
|
|
4713
5083
|
if (!result.ok || !result.body) return fail4("named_seat_refund_provider_unavailable");
|
|
4714
5084
|
return result.body;
|
|
4715
5085
|
};
|
|
@@ -4764,7 +5134,7 @@ async function requestNamedSeatRefund(db, input, now) {
|
|
|
4764
5134
|
const member = await membershipLoad(db, "networkMembership", String(seat.primaryMembershipId));
|
|
4765
5135
|
requireMembershipScope(member, input);
|
|
4766
5136
|
if (seat.status === "refunded" || seat.status === "refund_pending") return;
|
|
4767
|
-
if (!seat.purchasedAt || !seat.providerPaymentIntentId || input.reason === "declined" && seat.journeyApprovedAt !== void 0 || input.reason === "
|
|
5137
|
+
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");
|
|
4768
5138
|
const entitlement = seat.entitlementId ? await membershipLoad(db, "networkEntitlement", String(seat.entitlementId)) : void 0;
|
|
4769
5139
|
await db.transact([
|
|
4770
5140
|
membershipUpdate("networkSeat", input.seatId, { status: "refund_pending", refundReason: input.reason, revision: Number(seat.revision) + 1, updatedAt: now }),
|
|
@@ -4831,11 +5201,24 @@ async function confirmNamedSeatProviderRefund(db, input, now) {
|
|
|
4831
5201
|
const row2 = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
|
|
4832
5202
|
if (!row2) return membershipFail("named_seat_refund_evidence_pending");
|
|
4833
5203
|
const fact = row2.fact;
|
|
4834
|
-
|
|
5204
|
+
await commercialEvidenceGuard(db, input.eventId, row2.digest, "named_seat_refund_evidence_quarantined");
|
|
4835
5205
|
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");
|
|
4836
5206
|
await requestNamedSeatRefund(db, { ...input, reason: "provider_refund" }, now);
|
|
4837
5207
|
}
|
|
4838
5208
|
|
|
5209
|
+
// src/membership-seat-operator-refund.ts
|
|
5210
|
+
async function refundNamedSeatByOperator(db, input, stripeSecretKey, now) {
|
|
5211
|
+
const seat = await membershipLoad(db, "networkSeat", input.seatId);
|
|
5212
|
+
requireMembershipScope(seat, input);
|
|
5213
|
+
await requestNamedSeatRefund(db, { ...input, reason: "operator" }, now);
|
|
5214
|
+
await completeNamedSeatRefund(db, input.seatId, stripeSecretKey, now);
|
|
5215
|
+
const refunded = await membershipLoad(db, "networkSeat", input.seatId);
|
|
5216
|
+
if (refunded?.status !== "refunded" || !Number.isSafeInteger(refunded.refundedCents) || Number(refunded.refundedCents) <= 0) {
|
|
5217
|
+
throw new Error("named seat refund unconfirmed");
|
|
5218
|
+
}
|
|
5219
|
+
return { seatId: String(seat.id), status: "refunded", refundedCents: Number(refunded.refundedCents), subscriptionCanceled: false };
|
|
5220
|
+
}
|
|
5221
|
+
|
|
4839
5222
|
// src/membership-seat-offer.ts
|
|
4840
5223
|
async function namedSeatOfferDigest(offer) {
|
|
4841
5224
|
const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify([
|
|
@@ -4848,7 +5231,7 @@ async function namedSeatOfferDigest(offer) {
|
|
|
4848
5231
|
"named-only",
|
|
4849
5232
|
"full-upfront",
|
|
4850
5233
|
"auto-renew-coterm",
|
|
4851
|
-
"
|
|
5234
|
+
"no-automatic-refund"
|
|
4852
5235
|
])));
|
|
4853
5236
|
return [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
4854
5237
|
}
|
|
@@ -4888,7 +5271,7 @@ async function readNamedSeatInvitation(db, input, now) {
|
|
|
4888
5271
|
await loadMembershipAuthorityState(db, target);
|
|
4889
5272
|
const member = await membershipLoad(db, "networkMembership", String(seat.primaryMembershipId));
|
|
4890
5273
|
requireMembershipScope(member, target);
|
|
4891
|
-
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)
|
|
5274
|
+
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");
|
|
4892
5275
|
return {
|
|
4893
5276
|
seatId: input.seatId,
|
|
4894
5277
|
primaryApplicationId: target.applicationId,
|
|
@@ -4918,7 +5301,7 @@ async function readMembershipChapterEffect(db, input, now) {
|
|
|
4918
5301
|
requireMembershipScope(member, input);
|
|
4919
5302
|
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");
|
|
4920
5303
|
let applicable;
|
|
4921
|
-
if (input.kind === "invite_named_recipient") applicable = seat.status === "pending_acceptance" && seat.acceptedAt === void 0 && now < Number(
|
|
5304
|
+
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));
|
|
4922
5305
|
else if (input.kind === "revoke_named_recipient_access") applicable = ["refund_pending", "refunded"].includes(String(seat.status));
|
|
4923
5306
|
else applicable = (await readNamedSeatAccess(db, {
|
|
4924
5307
|
...input,
|
|
@@ -4947,6 +5330,7 @@ var MEMBERSHIP_SEAT_ACTION_FIELDS = {
|
|
|
4947
5330
|
"seat-reservations": ["recipientName", "recipientEmail", "purchaserEmail", "purchaserUserId"],
|
|
4948
5331
|
"seat-payments": ["seatId", "eventId"],
|
|
4949
5332
|
"seat-refunds": ["seatId", "eventId"],
|
|
5333
|
+
"seat-operator-refund": ["seatId"],
|
|
4950
5334
|
"seat-accept": recipient,
|
|
4951
5335
|
"seat-approve": recipient,
|
|
4952
5336
|
"seat-decline": recipient,
|
|
@@ -5015,6 +5399,7 @@ async function handleMembershipSeatAction(action, db, peer, input, now, stripeSe
|
|
|
5015
5399
|
if (refunded?.status !== "refunded" || refunded.refundedCents !== seat.amountCents) throw new Error("named seat refund unconfirmed");
|
|
5016
5400
|
return { seatId: seat.id, status: "refunded", refundedCents: refunded.refundedCents, subscriptionCanceled: false };
|
|
5017
5401
|
}
|
|
5402
|
+
if (action === "seat-operator-refund") return refundNamedSeatByOperator(db, { ...target, seatId: String(input.seatId) }, secret, now);
|
|
5018
5403
|
if (action === "seat-checkouts") return prepareNamedMembershipSeatPayment(db, {
|
|
5019
5404
|
...target,
|
|
5020
5405
|
priceId: peer.seatPriceId,
|
|
@@ -5103,7 +5488,7 @@ function createMembershipAuthorityHandler(options) {
|
|
|
5103
5488
|
if (req.method !== "POST") return respond({ error: "method_not_allowed" }, 405);
|
|
5104
5489
|
const sender = req.headers.get(import_db.FEDERATION_HEADERS.sender);
|
|
5105
5490
|
const action = url.pathname.split("/").at(-1);
|
|
5106
|
-
if (!["offers", "checkouts", "payments", "approvals", "refunds", "renewals", "access", ...Object.keys(MEMBERSHIP_SEAT_ACTION_FIELDS)].includes(action ?? "")) return respond({ error: "unauthorized" }, 401);
|
|
5491
|
+
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);
|
|
5107
5492
|
const peer = peers.find((candidate) => candidate.appId === sender && url.pathname === `${MEMBERSHIP_AUTHORITY_PATH}/${candidate.runtime}/${action}` && !url.search);
|
|
5108
5493
|
if (!peer) return respond({ error: "unauthorized" }, 401);
|
|
5109
5494
|
let body;
|
|
@@ -5125,13 +5510,23 @@ function createMembershipAuthorityHandler(options) {
|
|
|
5125
5510
|
let input;
|
|
5126
5511
|
try {
|
|
5127
5512
|
input = JSON.parse(verified.body);
|
|
5128
|
-
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] ?? []];
|
|
5513
|
+
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] ?? []];
|
|
5129
5514
|
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])))) {
|
|
5130
5515
|
throw new TypeError("invalid request");
|
|
5131
5516
|
}
|
|
5132
5517
|
} catch {
|
|
5133
5518
|
return respond({ error: "invalid_offer_request" }, 400);
|
|
5134
5519
|
}
|
|
5520
|
+
if (action === "offer-preview") {
|
|
5521
|
+
return respond({ ok: true, preview: await previewMembershipCommercialOffer(db, {
|
|
5522
|
+
appId: peer.appId,
|
|
5523
|
+
chapterId: peer.chapterId,
|
|
5524
|
+
environment: peer.environment,
|
|
5525
|
+
runtime: peer.runtime,
|
|
5526
|
+
skuId: String(input.skuId),
|
|
5527
|
+
now: options.now?.() ?? Date.now()
|
|
5528
|
+
}) });
|
|
5529
|
+
}
|
|
5135
5530
|
if (Object.hasOwn(MEMBERSHIP_SEAT_ACTION_FIELDS, action)) {
|
|
5136
5531
|
return respond({ ok: true, seat: await handleMembershipSeatAction(
|
|
5137
5532
|
action,
|
|
@@ -5150,6 +5545,15 @@ function createMembershipAuthorityHandler(options) {
|
|
|
5150
5545
|
applicationId: String(input.applicationId),
|
|
5151
5546
|
skuId: String(input.skuId)
|
|
5152
5547
|
};
|
|
5548
|
+
if (action === "restarts" || action === "cancel-renewal") {
|
|
5549
|
+
const stripeSecret = await getVaultSecret(db, options.stripeSecretName ?? "stripe_secret_key");
|
|
5550
|
+
if (!stripeSecret) return respond({ error: "membership_authority_unavailable" }, 503);
|
|
5551
|
+
if (action === "cancel-renewal") return respond({ ok: true, renewal: await cancelMembershipAtTermEnd(db, target, options.now?.() ?? Date.now(), stripeSecret) });
|
|
5552
|
+
return respond({ ok: true, restart: await prepareMembershipRestart(db, {
|
|
5553
|
+
...target,
|
|
5554
|
+
previousSubscriptionId: String(input.previousSubscriptionId)
|
|
5555
|
+
}, options.now?.() ?? Date.now(), stripeSecret) });
|
|
5556
|
+
}
|
|
5153
5557
|
if (action === "access") return respond({ ok: true, access: await readMembershipAccess(db, target, options.now?.() ?? Date.now()) });
|
|
5154
5558
|
if (action === "renewals") return respond({ ok: true, renewal: await reconcileMembershipBilling(
|
|
5155
5559
|
db,
|
|
@@ -5273,6 +5677,12 @@ async function requestMembershipCommercialOffer(options) {
|
|
|
5273
5677
|
lifetimeMs: 6e4
|
|
5274
5678
|
});
|
|
5275
5679
|
}
|
|
5680
|
+
async function requestMembershipOfferPreview(options) {
|
|
5681
|
+
const result = await requestAuthority(options, "offer-preview", { skuId: options.skuId });
|
|
5682
|
+
const preview = result.preview;
|
|
5683
|
+
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");
|
|
5684
|
+
return preview;
|
|
5685
|
+
}
|
|
5276
5686
|
async function requestMembershipCheckoutBinding(options) {
|
|
5277
5687
|
const result = await requestAuthority(options, "checkouts", {
|
|
5278
5688
|
applicationId: options.applicationId,
|
|
@@ -5311,6 +5721,12 @@ async function requestNamedSeatDecline(options) {
|
|
|
5311
5721
|
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");
|
|
5312
5722
|
return seat;
|
|
5313
5723
|
}
|
|
5724
|
+
async function requestNamedSeatOperatorRefund(options) {
|
|
5725
|
+
const result = await requestAuthority(options, "seat-operator-refund", { applicationId: options.applicationId, skuId: options.skuId, seatId: options.seatId });
|
|
5726
|
+
const seat = result.seat;
|
|
5727
|
+
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");
|
|
5728
|
+
return seat;
|
|
5729
|
+
}
|
|
5314
5730
|
async function requestNamedSeatReservation(options) {
|
|
5315
5731
|
const result = await requestAuthority(options, "seat-reservations", {
|
|
5316
5732
|
applicationId: options.applicationId,
|
|
@@ -5423,7 +5839,7 @@ async function requestMembershipRefund(options) {
|
|
|
5423
5839
|
async function requestMembershipRenewal(options) {
|
|
5424
5840
|
const result = await requestAuthority(options, "renewals", { applicationId: options.applicationId, skuId: options.skuId, eventId: options.eventId });
|
|
5425
5841
|
const row2 = result.renewal;
|
|
5426
|
-
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)) {
|
|
5842
|
+
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)) {
|
|
5427
5843
|
throw new Error("invalid membership renewal receipt");
|
|
5428
5844
|
}
|
|
5429
5845
|
return { membershipId: row2.membershipId, status: row2.status, termEndsAt: row2.termEndsAt, graceEndsAt: row2.graceEndsAt, autoRenew: row2.autoRenew };
|
|
@@ -5465,9 +5881,25 @@ async function requestMembershipAccess(options) {
|
|
|
5465
5881
|
const result = await requestAuthority(options, "access", { applicationId: options.applicationId, skuId: options.skuId });
|
|
5466
5882
|
return verifyMembershipAccessReceipt(result.access, options, Date.now());
|
|
5467
5883
|
}
|
|
5884
|
+
async function requestMembershipRestart(options) {
|
|
5885
|
+
const result = await requestAuthority(options, "restarts", {
|
|
5886
|
+
applicationId: options.applicationId,
|
|
5887
|
+
skuId: options.skuId,
|
|
5888
|
+
previousSubscriptionId: options.previousSubscriptionId
|
|
5889
|
+
});
|
|
5890
|
+
const row2 = result.restart;
|
|
5891
|
+
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");
|
|
5892
|
+
return { applicationId: row2.applicationId, membershipId: row2.membershipId, restartId: row2.restartId, memberNumber: row2.memberNumber, status: "ready" };
|
|
5893
|
+
}
|
|
5894
|
+
async function requestMembershipCancellation(options) {
|
|
5895
|
+
const result = await requestAuthority(options, "cancel-renewal", { applicationId: options.applicationId, skuId: options.skuId });
|
|
5896
|
+
const row2 = result.renewal;
|
|
5897
|
+
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");
|
|
5898
|
+
return { membershipId: row2.membershipId, status: row2.status, termEndsAt: row2.termEndsAt, graceEndsAt: 0, autoRenew: false };
|
|
5899
|
+
}
|
|
5468
5900
|
|
|
5469
5901
|
// src/membership-seat-cancel.ts
|
|
5470
|
-
var
|
|
5902
|
+
var import_stripe9 = require("@odla-ai/stripe");
|
|
5471
5903
|
async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
|
|
5472
5904
|
membershipClock(now);
|
|
5473
5905
|
const state = await loadMembershipAuthorityState(db, target);
|
|
@@ -5495,7 +5927,7 @@ async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
|
|
|
5495
5927
|
}
|
|
5496
5928
|
const metadata = { ...stripePartitionMetadata(target), applicationId: target.applicationId, membershipId: seat.primaryMembershipId, seatId: seat.id };
|
|
5497
5929
|
const request = async (method, path, params) => {
|
|
5498
|
-
const result = await (0,
|
|
5930
|
+
const result = await (0, import_stripe9.stripeRequest)(
|
|
5499
5931
|
{ secretKey },
|
|
5500
5932
|
method,
|
|
5501
5933
|
path,
|
|
@@ -5508,7 +5940,7 @@ async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
|
|
|
5508
5940
|
let payment;
|
|
5509
5941
|
if (seat.providerPaymentIntentId) payment = await request("GET", `/v1/payment_intents/${encodeURIComponent(String(seat.providerPaymentIntentId))}`);
|
|
5510
5942
|
else {
|
|
5511
|
-
const snapshot = await listStripeObjects((0,
|
|
5943
|
+
const snapshot = await listStripeObjects((0, import_stripe9.initStripe)({ secretKey }), "/v1/payment_intents", { customer: String(seat.providerCustomerId) });
|
|
5512
5944
|
const matches2 = snapshot.objects.filter((row2) => row2.metadata?.seatId === target.seatId);
|
|
5513
5945
|
if (!snapshot.complete || matches2.length !== 1) throw new Error("seat payment creation outcome unresolved");
|
|
5514
5946
|
payment = matches2[0];
|
|
@@ -5539,7 +5971,7 @@ async function drainMembershipEffects(db, options, now) {
|
|
|
5539
5971
|
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100 || !options.stripeSecretKey) throw new TypeError("invalid membership recovery configuration");
|
|
5540
5972
|
await loadMembershipAuthorityState(db, scope2);
|
|
5541
5973
|
const matches2 = (row2) => Object.entries(scope2).every(([key3, value]) => row2[key3] === value);
|
|
5542
|
-
let
|
|
5974
|
+
let pending = 0, complete = 0, more = false;
|
|
5543
5975
|
for (const status2 of ["payment_pending", "payment_processing", "payment_canceling"]) {
|
|
5544
5976
|
const seats = (await db.query({ networkSeat: { $: { where: { ...scope2, status: status2 }, order: { updatedAt: "asc" }, limit: limit + 1 } } })).networkSeat ?? [];
|
|
5545
5977
|
more ||= seats.length > limit;
|
|
@@ -5557,26 +5989,6 @@ async function drainMembershipEffects(db, options, now) {
|
|
|
5557
5989
|
}
|
|
5558
5990
|
}
|
|
5559
5991
|
}
|
|
5560
|
-
for (const status2 of ["renewal_pending", "pending_acceptance"]) {
|
|
5561
|
-
const seats = (await db.query({ networkSeat: { $: { where: { ...scope2, status: status2 }, order: { acceptanceEndsAt: "asc" }, limit: limit + 1 } } })).networkSeat ?? [];
|
|
5562
|
-
more ||= seats.length > limit;
|
|
5563
|
-
for (const seat of seats.slice(0, limit)) {
|
|
5564
|
-
if (!matches2(seat)) throw new Error("membership recovery scope mismatch");
|
|
5565
|
-
if (seat.acceptedAt !== void 0 || now < Number(seat.acceptanceEndsAt)) continue;
|
|
5566
|
-
try {
|
|
5567
|
-
await requestNamedSeatRefund(db, {
|
|
5568
|
-
...scope2,
|
|
5569
|
-
applicationId: String(seat.applicationId),
|
|
5570
|
-
skuId: String(seat.skuId),
|
|
5571
|
-
seatId: String(seat.id),
|
|
5572
|
-
reason: "expired"
|
|
5573
|
-
}, now);
|
|
5574
|
-
expired++;
|
|
5575
|
-
} catch {
|
|
5576
|
-
pending++;
|
|
5577
|
-
}
|
|
5578
|
-
}
|
|
5579
|
-
}
|
|
5580
5992
|
const effects = (await db.query({ membershipEffect: { $: { where: {
|
|
5581
5993
|
chapterId: scope2.chapterId,
|
|
5582
5994
|
environment: scope2.environment,
|
|
@@ -5638,7 +6050,7 @@ async function drainMembershipEffects(db, options, now) {
|
|
|
5638
6050
|
pending++;
|
|
5639
6051
|
}
|
|
5640
6052
|
}
|
|
5641
|
-
return { complete, pending,
|
|
6053
|
+
return { complete, pending, more };
|
|
5642
6054
|
}
|
|
5643
6055
|
|
|
5644
6056
|
// src/schema-membership-offers.ts
|
|
@@ -5732,7 +6144,27 @@ var membershipAuthorityEntities = {
|
|
|
5732
6144
|
cancellationRequestedAt: attr9("number", true),
|
|
5733
6145
|
graceEndsAt: attr9("number", true),
|
|
5734
6146
|
...times(),
|
|
5735
|
-
refundRequestedAt: attr9("number", true)
|
|
6147
|
+
refundRequestedAt: attr9("number", true),
|
|
6148
|
+
restartId: key2(true)
|
|
6149
|
+
} },
|
|
6150
|
+
membershipRestart: { attrs: {
|
|
6151
|
+
id: id9(),
|
|
6152
|
+
...scope(),
|
|
6153
|
+
membershipId: key2(),
|
|
6154
|
+
restartId: key2(),
|
|
6155
|
+
previousSubscriptionId: key2(),
|
|
6156
|
+
memberNumber: attr9("number"),
|
|
6157
|
+
status: key2(),
|
|
6158
|
+
createdAt: attr9("number")
|
|
6159
|
+
} },
|
|
6160
|
+
membershipPurchaseHistory: { attrs: {
|
|
6161
|
+
id: id9(),
|
|
6162
|
+
...scope(),
|
|
6163
|
+
membershipId: key2(),
|
|
6164
|
+
providerSubscriptionId: key2(),
|
|
6165
|
+
memberSnapshot: attr9("json"),
|
|
6166
|
+
bindingSnapshot: attr9("json"),
|
|
6167
|
+
archivedAt: attr9("number")
|
|
5736
6168
|
} },
|
|
5737
6169
|
networkEntitlement: { attrs: {
|
|
5738
6170
|
id: id9(),
|
|
@@ -6531,7 +6963,7 @@ async function reconcileLifecycleDelivery(db, input, now) {
|
|
|
6531
6963
|
}
|
|
6532
6964
|
|
|
6533
6965
|
// src/payments.ts
|
|
6534
|
-
var
|
|
6966
|
+
var import_stripe10 = require("@odla-ai/stripe");
|
|
6535
6967
|
function paymentsReady(group, hasSecretKey) {
|
|
6536
6968
|
return Boolean(group.stripePublishableKey && group.stripePriceId && hasSecretKey);
|
|
6537
6969
|
}
|
|
@@ -7158,9 +7590,9 @@ function sharedRecordFromCrm(crm, record4, target, sourceId) {
|
|
|
7158
7590
|
}
|
|
7159
7591
|
const nameField = def.nameField ?? "name";
|
|
7160
7592
|
const input = {};
|
|
7161
|
-
for (const
|
|
7162
|
-
const value = record4.fields?.[
|
|
7163
|
-
if (value !== void 0) input[
|
|
7593
|
+
for (const field2 of /* @__PURE__ */ new Set([nameField, ...fields])) {
|
|
7594
|
+
const value = record4.fields?.[field2];
|
|
7595
|
+
if (value !== void 0) input[field2] = value;
|
|
7164
7596
|
}
|
|
7165
7597
|
if (input[nameField] === void 0) input[nameField] = record4.name;
|
|
7166
7598
|
return sourceId ? { version: 2, source: { siteId: sourceId, recordId: record4.id }, type: record4.type, input } : { version: 1, type: record4.type, hubRecordId: record4.id, input };
|
|
@@ -7429,9 +7861,9 @@ function subAnnualCents(sub) {
|
|
|
7429
7861
|
}
|
|
7430
7862
|
|
|
7431
7863
|
// src/payments-stripe.ts
|
|
7432
|
-
var
|
|
7864
|
+
var import_stripe11 = require("@odla-ai/stripe");
|
|
7433
7865
|
async function stripeCall(sk, method, path, params, idempotencyKey) {
|
|
7434
|
-
return (0,
|
|
7866
|
+
return (0, import_stripe11.stripeRequest)(
|
|
7435
7867
|
{ secretKey: sk },
|
|
7436
7868
|
method,
|
|
7437
7869
|
path,
|
|
@@ -7741,7 +8173,8 @@ function applicationSummary(app) {
|
|
|
7741
8173
|
status: app.status,
|
|
7742
8174
|
createdAt: app.createdAt ?? null,
|
|
7743
8175
|
meetingLink: app.meetingLink ?? null,
|
|
7744
|
-
paid: Boolean(app.stripeSubscriptionId) && app.status !== "refunded",
|
|
8176
|
+
paid: Boolean(app.stripeSubscriptionId) && app.status !== "refunded" && app.canceled !== true && !["lapsed", "refunded", "refund_pending", "canceled", "restarting"].includes(app.membershipStatus ?? ""),
|
|
8177
|
+
...app.membershipStatus ? { membershipStatus: app.membershipStatus, membershipGraceEndsAt: app.membershipGraceEndsAt, membershipAutoRenew: app.membershipAutoRenew } : {},
|
|
7745
8178
|
renewalAt: app.renewalAt ?? null,
|
|
7746
8179
|
canceled: app.canceled === true,
|
|
7747
8180
|
tierId: app.tierId ?? null
|
|
@@ -7923,7 +8356,7 @@ function isValidTimeZone(tz) {
|
|
|
7923
8356
|
function resolveScheduling(config) {
|
|
7924
8357
|
const result = validateScheduling(config);
|
|
7925
8358
|
if (result.ok) return result.value;
|
|
7926
|
-
const detail = Object.entries(result.errors).map(([
|
|
8359
|
+
const detail = Object.entries(result.errors).map(([field2, message]) => `${field2}: ${message}`).join(" ");
|
|
7927
8360
|
throw new Error(`scheduling: ${detail}`);
|
|
7928
8361
|
}
|
|
7929
8362
|
function validateScheduling(config) {
|
|
@@ -9068,7 +9501,7 @@ var MEMBER_SELF_READABLE_FIELDS = new Set(
|
|
|
9068
9501
|
MEMBER_SYSTEM_AUTHORITY_MATRIX.filter((row2) => row2.memberAccess !== "none").map((row2) => row2.id)
|
|
9069
9502
|
);
|
|
9070
9503
|
function projectMemberSelf(record4) {
|
|
9071
|
-
return Object.fromEntries(Object.entries(record4).filter(([
|
|
9504
|
+
return Object.fromEntries(Object.entries(record4).filter(([field2]) => MEMBER_SELF_READABLE_FIELDS.has(field2)));
|
|
9072
9505
|
}
|
|
9073
9506
|
function classifyMemberSystemWrite(intent) {
|
|
9074
9507
|
if (intent.actor === "bnf") return "reject_bnf_surface";
|