@odla-ai/chapter 0.48.3 → 0.49.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-KZ2AXEZW.js → chunk-KHDO2QFA.js} +749 -543
- package/dist/chunk-KHDO2QFA.js.map +1 -0
- package/dist/{chunk-MQ4OPZCS.js → chunk-KXIGZWG5.js} +720 -615
- package/dist/chunk-KXIGZWG5.js.map +1 -0
- package/dist/{chunk-4HS2JPE2.js → chunk-N2JIKINY.js} +17 -1
- package/dist/chunk-N2JIKINY.js.map +1 -0
- package/dist/{copy-context-CMupQL9h.d.ts → copy-context-By4Nq7yB.d.ts} +6 -1
- package/dist/index.cjs +488 -118
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +50 -3
- package/dist/index.d.ts +50 -3
- package/dist/index.js +484 -114
- package/dist/index.js.map +1 -1
- package/dist/ui/admin/index.d.ts +14 -3
- package/dist/ui/admin/index.js +6 -2
- package/dist/ui/index.d.ts +3 -3
- package/dist/ui/index.js +11 -3
- package/dist/ui/member/index.d.ts +36 -4
- package/dist/ui/member/index.js +6 -2
- package/dist/worker/index.cjs +7109 -6318
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +24 -1
- package/dist/worker/index.d.ts +24 -1
- package/dist/worker/index.js +7239 -6448
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-4HS2JPE2.js.map +0 -1
- package/dist/chunk-KZ2AXEZW.js.map +0 -1
- package/dist/chunk-MQ4OPZCS.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -204,12 +204,14 @@ __export(src_exports, {
|
|
|
204
204
|
renewalPatch: () => renewalPatch,
|
|
205
205
|
requestMembershipAccess: () => requestMembershipAccess,
|
|
206
206
|
requestMembershipApproval: () => requestMembershipApproval,
|
|
207
|
+
requestMembershipCancellation: () => requestMembershipCancellation,
|
|
207
208
|
requestMembershipChapterEffect: () => requestMembershipChapterEffect,
|
|
208
209
|
requestMembershipCheckoutBinding: () => requestMembershipCheckoutBinding,
|
|
209
210
|
requestMembershipCommercialOffer: () => requestMembershipCommercialOffer,
|
|
210
211
|
requestMembershipPayment: () => requestMembershipPayment,
|
|
211
212
|
requestMembershipRefund: () => requestMembershipRefund,
|
|
212
213
|
requestMembershipRenewal: () => requestMembershipRenewal,
|
|
214
|
+
requestMembershipRestart: () => requestMembershipRestart,
|
|
213
215
|
requestNamedSeatAcceptance: () => requestNamedSeatAcceptance,
|
|
214
216
|
requestNamedSeatAccess: () => requestNamedSeatAccess,
|
|
215
217
|
requestNamedSeatApproval: () => requestNamedSeatApproval,
|
|
@@ -241,7 +243,7 @@ __export(src_exports, {
|
|
|
241
243
|
slotWindow: () => slotWindow,
|
|
242
244
|
stageIndex: () => stageIndex,
|
|
243
245
|
stripeCall: () => stripeCall,
|
|
244
|
-
stripeForm: () =>
|
|
246
|
+
stripeForm: () => import_stripe10.stripeForm,
|
|
245
247
|
subAnnualCents: () => subAnnualCents,
|
|
246
248
|
submitApplication: () => submitApplication,
|
|
247
249
|
subscriptionIdempotencyKey: () => subscriptionIdempotencyKey,
|
|
@@ -262,7 +264,7 @@ __export(src_exports, {
|
|
|
262
264
|
verifyMembershipAccessReceipt: () => verifyMembershipAccessReceipt,
|
|
263
265
|
verifyMembershipCheckoutBinding: () => verifyMembershipCheckoutBinding,
|
|
264
266
|
verifyMembershipPaymentReceipt: () => verifyMembershipPaymentReceipt,
|
|
265
|
-
verifyStripeSignature: () =>
|
|
267
|
+
verifyStripeSignature: () => import_stripe10.verifyStripeSignature,
|
|
266
268
|
webhookMutationId: () => webhookMutationId
|
|
267
269
|
});
|
|
268
270
|
module.exports = __toCommonJS(src_exports);
|
|
@@ -307,12 +309,12 @@ function resolveApplication(a) {
|
|
|
307
309
|
if (!Array.isArray(arr) || !arr.every((f) => typeof f === "string" && f !== "")) {
|
|
308
310
|
throw new Error(`defineChapter.application.${name}: must be an array of field-name strings`);
|
|
309
311
|
}
|
|
310
|
-
const unsafe = arr.find((
|
|
312
|
+
const unsafe = arr.find((field2) => !SCHEMA_FIELD.test(field2));
|
|
311
313
|
if (unsafe) {
|
|
312
314
|
throw new Error(`defineChapter.application.${name}: field "${unsafe}" must match ${SCHEMA_FIELD.source}`);
|
|
313
315
|
}
|
|
314
316
|
}
|
|
315
|
-
const duplicate = [...required, ...optional].find((
|
|
317
|
+
const duplicate = [...required, ...optional].find((field2, index2, fields) => fields.indexOf(field2) !== index2);
|
|
316
318
|
if (duplicate) {
|
|
317
319
|
throw new Error(`defineChapter.application: duplicate field "${duplicate}" across required/optional lists`);
|
|
318
320
|
}
|
|
@@ -323,14 +325,14 @@ function resolveApplication(a) {
|
|
|
323
325
|
throw new Error("defineChapter.application.crmFields: must be an array of field-name strings");
|
|
324
326
|
}
|
|
325
327
|
const conditions = a?.conditions ?? {};
|
|
326
|
-
for (const [
|
|
328
|
+
for (const [field2, declared] of Object.entries(conditions)) {
|
|
327
329
|
for (const key3 of ["visibleWhen", "requiredWhen"]) {
|
|
328
330
|
const expression = declared?.[key3];
|
|
329
331
|
if (expression === void 0) continue;
|
|
330
332
|
if (typeof expression !== "string" || !expression.trim()) {
|
|
331
|
-
throw new Error(`defineChapter.application.conditions.${
|
|
333
|
+
throw new Error(`defineChapter.application.conditions.${field2}.${key3}: must be a non-empty CEL condition`);
|
|
332
334
|
}
|
|
333
|
-
(0, import_crm.assertFieldCondition)(expression, `defineChapter.application.conditions.${
|
|
335
|
+
(0, import_crm.assertFieldCondition)(expression, `defineChapter.application.conditions.${field2}.${key3}`);
|
|
334
336
|
}
|
|
335
337
|
}
|
|
336
338
|
return {
|
|
@@ -401,6 +403,13 @@ async function submitApplication(db, chapter, fields, opts) {
|
|
|
401
403
|
if (opts.groupId) row2.groupId = opts.groupId;
|
|
402
404
|
if (opts.runtime) row2.stripeRuntime = opts.runtime;
|
|
403
405
|
if (chapter.account !== "none") row2.clerkAccountPending = true;
|
|
406
|
+
if (opts.namedSeat) Object.assign(row2, {
|
|
407
|
+
namedSeatId: opts.namedSeat.seatId,
|
|
408
|
+
namedSeatPrimaryApplicationId: opts.namedSeat.primaryApplicationId,
|
|
409
|
+
namedSeatClaimPending: true,
|
|
410
|
+
clerkUserId: opts.namedSeat.userId,
|
|
411
|
+
clerkAccountPending: false
|
|
412
|
+
});
|
|
404
413
|
if (typeof fields.email === "string" && isValidEmail(fields.email.trim())) {
|
|
405
414
|
row2.emailNormalized = normalizeChapterEmail(fields.email);
|
|
406
415
|
row2.signupEmailKey = row2.emailNormalized;
|
|
@@ -530,6 +539,10 @@ var applicationAttrs = {
|
|
|
530
539
|
giftPaidAt: attr("number", { optional: true, indexed: true }),
|
|
531
540
|
giftRevokedAt: attr("number", { optional: true, indexed: true }),
|
|
532
541
|
giftCheckoutStartedAt: attr("number", { optional: true, indexed: true }),
|
|
542
|
+
membershipRestartId: attr("string", { optional: true, indexed: true }),
|
|
543
|
+
membershipStatus: attr("string", { optional: true }),
|
|
544
|
+
membershipGraceEndsAt: attr("number", { optional: true }),
|
|
545
|
+
membershipAutoRenew: attr("boolean", { optional: true }),
|
|
533
546
|
namedSeatId: attr("string", { optional: true, unique: true, indexed: true }),
|
|
534
547
|
namedSeatPrimaryApplicationId: attr("string", { optional: true }),
|
|
535
548
|
namedSeatClaimPending: attr("boolean", { optional: true, indexed: true }),
|
|
@@ -540,19 +553,19 @@ function applicationsFor(application) {
|
|
|
540
553
|
for (const [name, spec] of Object.entries(applicationAttrs)) {
|
|
541
554
|
attrs[name] = { ...spec, optional: APPLICATION_INPUT_FIELDS.has(name) ? true : spec.optional };
|
|
542
555
|
}
|
|
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,
|
|
556
|
+
const apply = (field2, optional) => {
|
|
557
|
+
const existing = Object.hasOwn(attrs, field2) ? attrs[field2] : void 0;
|
|
558
|
+
if (existing && !APPLICATION_INPUT_FIELDS.has(field2)) throw new Error(`defineChapter.application: field "${field2}" is reserved by the applications schema`);
|
|
559
|
+
if (existing && existing.type !== "string") throw new Error(`defineChapter.application: field "${field2}" is not a configurable string field`);
|
|
560
|
+
Object.defineProperty(attrs, field2, {
|
|
548
561
|
value: existing ? { ...existing, optional } : attr("string", { optional }),
|
|
549
562
|
enumerable: true,
|
|
550
563
|
configurable: true,
|
|
551
564
|
writable: true
|
|
552
565
|
});
|
|
553
566
|
};
|
|
554
|
-
for (const
|
|
555
|
-
for (const
|
|
567
|
+
for (const field2 of application.required) apply(field2, false);
|
|
568
|
+
for (const field2 of application.optional) apply(field2, true);
|
|
556
569
|
return { attrs };
|
|
557
570
|
}
|
|
558
571
|
|
|
@@ -759,6 +772,19 @@ var giftRedemptionAttemptsEntity = { attrs: {
|
|
|
759
772
|
updatedAt: attr4("number")
|
|
760
773
|
} };
|
|
761
774
|
|
|
775
|
+
// src/payment-recovery-schema.ts
|
|
776
|
+
var field = (type, unique = false) => ({ type, unique, indexed: type !== "json", optional: false });
|
|
777
|
+
var paymentRecoveryReviewsEntity = { attrs: {
|
|
778
|
+
id: field("string", true),
|
|
779
|
+
eventId: field("string"),
|
|
780
|
+
actorUserId: field("string"),
|
|
781
|
+
reviewedAt: field("number"),
|
|
782
|
+
previousDigest: field("string"),
|
|
783
|
+
providerDigest: field("string"),
|
|
784
|
+
scope: field("string"),
|
|
785
|
+
previousState: field("json")
|
|
786
|
+
} };
|
|
787
|
+
|
|
762
788
|
// src/schema-stripe.ts
|
|
763
789
|
var attr5 = (type, flags = {}) => ({
|
|
764
790
|
type,
|
|
@@ -779,6 +805,10 @@ var stripeEventReceiptsEntity = {
|
|
|
779
805
|
dataEnvironment: attr5("string", { indexed: true, optional: true }),
|
|
780
806
|
runtime: attr5("string", { indexed: true, optional: true }),
|
|
781
807
|
chapterId: attr5("string", { indexed: true, optional: true }),
|
|
808
|
+
retryReady: attr5("boolean", { optional: true }),
|
|
809
|
+
reviewedDigest: attr5("string", { optional: true }),
|
|
810
|
+
reviewId: attr5("string", { optional: true }),
|
|
811
|
+
collisionDigest: attr5("string", { optional: true }),
|
|
782
812
|
receivedAt: attr5("number", { indexed: true })
|
|
783
813
|
}
|
|
784
814
|
};
|
|
@@ -803,6 +833,7 @@ var subscriptionCheckoutIntentsEntity = {
|
|
|
803
833
|
requestedCustomerId: attr5("string", { indexed: true, optional: true }),
|
|
804
834
|
providerCustomerId: attr5("string", { indexed: true, optional: true }),
|
|
805
835
|
providerSubscriptionId: attr5("string", { unique: true, indexed: true, optional: true }),
|
|
836
|
+
purchaseAttemptId: attr5("string", { optional: true }),
|
|
806
837
|
status: attr5("string", { indexed: true }),
|
|
807
838
|
createdAt: attr5("number", { indexed: true }),
|
|
808
839
|
updatedAt: attr5("number", { indexed: true })
|
|
@@ -827,6 +858,20 @@ var membershipQuoteProjectionsEntity = {
|
|
|
827
858
|
quote: attr5("json", { optional: true })
|
|
828
859
|
}
|
|
829
860
|
};
|
|
861
|
+
var membershipCheckoutHistoryEntity = { attrs: {
|
|
862
|
+
id: attr5("string", { unique: true, indexed: true }),
|
|
863
|
+
applicationId: attr5("string", { indexed: true }),
|
|
864
|
+
appId: attr5("string"),
|
|
865
|
+
chapterId: attr5("string"),
|
|
866
|
+
environment: attr5("string"),
|
|
867
|
+
runtime: attr5("string"),
|
|
868
|
+
skuId: attr5("string"),
|
|
869
|
+
providerSubscriptionId: attr5("string", { indexed: true }),
|
|
870
|
+
applicationSnapshot: attr5("json"),
|
|
871
|
+
intentSnapshot: attr5("json"),
|
|
872
|
+
quoteSnapshot: attr5("json"),
|
|
873
|
+
archivedAt: attr5("number")
|
|
874
|
+
} };
|
|
830
875
|
|
|
831
876
|
// src/schema-network-person.ts
|
|
832
877
|
var string = (flags = {}) => ({
|
|
@@ -960,15 +1005,19 @@ var exceptions = { attrs: {
|
|
|
960
1005
|
kind: string2({ indexed: true }),
|
|
961
1006
|
sourceKey: string2({ indexed: true, optional: true }),
|
|
962
1007
|
status: string2({ indexed: true }),
|
|
1008
|
+
resolvedDigest: string2({ optional: true }),
|
|
1009
|
+
incomingDigest: string2({ optional: true }),
|
|
963
1010
|
detail: json2(),
|
|
964
1011
|
updatedAt: number2({ indexed: true })
|
|
965
1012
|
} };
|
|
966
1013
|
var networkCommercialEntities = {
|
|
1014
|
+
paymentRecoveryReviews: paymentRecoveryReviewsEntity,
|
|
967
1015
|
networkCommercialEvents: events,
|
|
968
1016
|
networkCommercialCustomers: projection(),
|
|
969
1017
|
networkCommercialSubscriptions: projection(),
|
|
970
1018
|
networkCommercialInvoices: projection(),
|
|
971
1019
|
networkCommercialRefunds: projection(),
|
|
1020
|
+
networkCommercialSeats: projection(),
|
|
972
1021
|
networkCommercialExceptions: exceptions
|
|
973
1022
|
};
|
|
974
1023
|
|
|
@@ -1171,8 +1220,9 @@ function chapterDb(mode, auth, includeNetworkNotes = false, application = resolv
|
|
|
1171
1220
|
entities.tiers = tiers;
|
|
1172
1221
|
entities.meetings = meetings;
|
|
1173
1222
|
entities.emailLog = emailLog;
|
|
1223
|
+
entities.paymentRecoveryReviews = paymentRecoveryReviewsEntity;
|
|
1174
1224
|
entities.stripeEventReceipts = stripeEventReceiptsEntity;
|
|
1175
|
-
entities
|
|
1225
|
+
Object.assign(entities, { subscriptionCheckoutIntents: subscriptionCheckoutIntentsEntity, membershipCheckoutHistory: membershipCheckoutHistoryEntity });
|
|
1176
1226
|
entities.membershipQuoteProjections = membershipQuoteProjectionsEntity;
|
|
1177
1227
|
entities.namedSeatConsents = namedSeatConsentsEntity;
|
|
1178
1228
|
entities.admissionGrants = admissionGrantsEntity;
|
|
@@ -1710,6 +1760,19 @@ var DEFAULT_COMMON_COPY = {
|
|
|
1710
1760
|
unknown: "(unknown)"
|
|
1711
1761
|
};
|
|
1712
1762
|
var DEFAULT_JOIN_COPY = {
|
|
1763
|
+
namedSeat: {
|
|
1764
|
+
title: "Your membership invitation",
|
|
1765
|
+
from: "{purchaserName} invited you.",
|
|
1766
|
+
prepaid: "Your {tierName} seat is already paid for.",
|
|
1767
|
+
body: "Accept your invitation, then complete your own application requirements. You will not be asked to pay for this seat.",
|
|
1768
|
+
consent: "I accept this membership for myself. It cannot be transferred and remains current while the purchaser's membership and my seat are current.",
|
|
1769
|
+
accept: "Accept my invitation",
|
|
1770
|
+
accepting: "Confirming your invitation\u2026",
|
|
1771
|
+
loading: "Loading your invitation\u2026",
|
|
1772
|
+
unavailable: "Your invitation is unavailable. Sign in with the invited email address, then try again.",
|
|
1773
|
+
retry: "Try again",
|
|
1774
|
+
failure: "We could not confirm acceptance. Retry this invitation; you do not need to purchase another membership."
|
|
1775
|
+
},
|
|
1713
1776
|
form: {
|
|
1714
1777
|
submit: "Submit application",
|
|
1715
1778
|
submitting: "Submitting\u2026",
|
|
@@ -1717,6 +1780,8 @@ var DEFAULT_JOIN_COPY = {
|
|
|
1717
1780
|
unexpectedFailure: "Something went wrong. Please try again."
|
|
1718
1781
|
},
|
|
1719
1782
|
booking: {
|
|
1783
|
+
title: "Book your introduction",
|
|
1784
|
+
intro: "Choose a time to meet your chapter and get to know each other.",
|
|
1720
1785
|
unavailable: "Scheduling is briefly unavailable. We'll reach out by email to arrange your call.",
|
|
1721
1786
|
loadFailed: "Times are briefly unavailable. Please try again.",
|
|
1722
1787
|
slotTaken: "That time was just taken. Here are the current openings.",
|
|
@@ -1908,16 +1973,16 @@ function resolveFormationFlowFields(crm, formation) {
|
|
|
1908
1973
|
if (!formation.enabled) return [];
|
|
1909
1974
|
const def = crm.type(formation.type);
|
|
1910
1975
|
return [...formation.required, ...formation.optional].map((id10, order2) => {
|
|
1911
|
-
const
|
|
1976
|
+
const field2 = def.fields[id10];
|
|
1912
1977
|
return {
|
|
1913
1978
|
id: id10,
|
|
1914
|
-
label:
|
|
1915
|
-
type:
|
|
1979
|
+
label: field2.label ?? humanize(id10),
|
|
1980
|
+
type: field2.type,
|
|
1916
1981
|
required: formation.required.includes(id10),
|
|
1917
1982
|
order: order2,
|
|
1918
1983
|
maxLength: formation.maxLen[id10] ?? formation.defaultMaxLen,
|
|
1919
|
-
...
|
|
1920
|
-
...id10 === def.emailField ||
|
|
1984
|
+
...field2.options ? { options: [...field2.options] } : {},
|
|
1985
|
+
...id10 === def.emailField || field2.type === "email" ? { emailIdentity: "literal" } : {}
|
|
1921
1986
|
};
|
|
1922
1987
|
});
|
|
1923
1988
|
}
|
|
@@ -1953,19 +2018,19 @@ function resolveLeaderFormation(input, crm) {
|
|
|
1953
2018
|
const required = input.required ?? (type === "chapter_application" ? DEFAULT_REQUIRED2 : []);
|
|
1954
2019
|
const optional = input.optional ?? (type === "chapter_application" ? DEFAULT_OPTIONAL2 : []);
|
|
1955
2020
|
for (const [name, values] of [["required", required], ["optional", optional]]) {
|
|
1956
|
-
if (!Array.isArray(values) || !values.every((
|
|
2021
|
+
if (!Array.isArray(values) || !values.every((field2) => typeof field2 === "string" && field2 !== "")) {
|
|
1957
2022
|
throw new Error(`defineChapter.formation.${name}: must be an array of field-name strings`);
|
|
1958
2023
|
}
|
|
1959
2024
|
}
|
|
1960
|
-
const duplicates = [...required, ...optional].filter((
|
|
2025
|
+
const duplicates = [...required, ...optional].filter((field2, index2, fields) => fields.indexOf(field2) !== index2);
|
|
1961
2026
|
if (duplicates.length) throw new Error(`defineChapter.formation: duplicate public field "${duplicates[0]}"`);
|
|
1962
|
-
const unknown = [...required, ...optional].filter((
|
|
2027
|
+
const unknown = [...required, ...optional].filter((field2) => !def.fields[field2]);
|
|
1963
2028
|
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(([,
|
|
2029
|
+
const missingRequired = Object.entries(def.fields).filter(([, field2]) => field2.required).map(([field2]) => field2).filter((field2) => !required.includes(field2));
|
|
1965
2030
|
if (missingRequired.length) {
|
|
1966
2031
|
throw new Error(`defineChapter.formation.required: must include required CRM field "${missingRequired[0]}"`);
|
|
1967
2032
|
}
|
|
1968
|
-
const unsupportedRequired = required.find((
|
|
2033
|
+
const unsupportedRequired = required.find((field2) => !["string", "email", "url", "enum"].includes(def.fields[field2].type));
|
|
1969
2034
|
if (unsupportedRequired) {
|
|
1970
2035
|
throw new Error(`defineChapter.formation.required: field "${unsupportedRequired}" must be string-like`);
|
|
1971
2036
|
}
|
|
@@ -1978,9 +2043,9 @@ function resolveLeaderFormation(input, crm) {
|
|
|
1978
2043
|
if (!Number.isSafeInteger(bodyCap) || bodyCap < 1024 || bodyCap > 131072) {
|
|
1979
2044
|
throw new Error("defineChapter.formation.bodyCap: must be an integer from 1024 to 131072");
|
|
1980
2045
|
}
|
|
1981
|
-
for (const [
|
|
1982
|
-
if (![...required, ...optional].includes(
|
|
1983
|
-
throw new Error(`defineChapter.formation.maxLen.${
|
|
2046
|
+
for (const [field2, cap] of Object.entries(input.maxLen ?? {})) {
|
|
2047
|
+
if (![...required, ...optional].includes(field2) || !Number.isSafeInteger(cap) || cap < 1 || cap > 32768) {
|
|
2048
|
+
throw new Error(`defineChapter.formation.maxLen.${field2}: must name a public field and be an integer from 1 to 32768`);
|
|
1984
2049
|
}
|
|
1985
2050
|
}
|
|
1986
2051
|
return {
|
|
@@ -2070,10 +2135,10 @@ function resolveNetwork(config, crm) {
|
|
|
2070
2135
|
}
|
|
2071
2136
|
const def = crm.config.types[type];
|
|
2072
2137
|
if (!def) throw new Error(`defineChapter.network.targets.${target.id}.fields: unknown leader CRM type "${type}"`);
|
|
2073
|
-
const unknown = names.filter((
|
|
2138
|
+
const unknown = names.filter((field2) => !def.fields[field2]);
|
|
2074
2139
|
if (unknown.length > 0) {
|
|
2075
2140
|
throw new Error(
|
|
2076
|
-
`defineChapter.network.targets.${target.id}.fields.${type}: ${unknown.map((
|
|
2141
|
+
`defineChapter.network.targets.${target.id}.fields.${type}: ${unknown.map((field2) => `"${field2}"`).join(", ")} not declared on the leader CRM type`
|
|
2077
2142
|
);
|
|
2078
2143
|
}
|
|
2079
2144
|
const nameField = def.nameField ?? "name";
|
|
@@ -2161,10 +2226,10 @@ function resolveNetwork(config, crm) {
|
|
|
2161
2226
|
if (!def) {
|
|
2162
2227
|
throw new Error(`defineChapter.network.readers.${reader.id}.fields: unknown local CRM type "${type}"`);
|
|
2163
2228
|
}
|
|
2164
|
-
const unknown = names.filter((
|
|
2229
|
+
const unknown = names.filter((field2) => !def.fields[field2]);
|
|
2165
2230
|
if (unknown.length > 0) {
|
|
2166
2231
|
throw new Error(
|
|
2167
|
-
`defineChapter.network.readers.${reader.id}.fields.${type}: ${unknown.map((
|
|
2232
|
+
`defineChapter.network.readers.${reader.id}.fields.${type}: ${unknown.map((field2) => `"${field2}"`).join(", ")} not declared on the local CRM type`
|
|
2168
2233
|
);
|
|
2169
2234
|
}
|
|
2170
2235
|
const nameField = def.nameField ?? "name";
|
|
@@ -2482,6 +2547,48 @@ function defineChapter(config) {
|
|
|
2482
2547
|
return chapter;
|
|
2483
2548
|
}
|
|
2484
2549
|
|
|
2550
|
+
// src/network-commercial-seats.ts
|
|
2551
|
+
function commercialSeatItems(object2) {
|
|
2552
|
+
const items = object2.items;
|
|
2553
|
+
if (!items || items.has_more !== false || !Array.isArray(items.data) || items.data.length > 100) return void 0;
|
|
2554
|
+
const result = [];
|
|
2555
|
+
for (const item of items.data) {
|
|
2556
|
+
const metadata = item.metadata;
|
|
2557
|
+
if (!metadata?.seatId || !metadata.membershipId) continue;
|
|
2558
|
+
const price = typeof item.price === "string" ? item.price : item.price?.id;
|
|
2559
|
+
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;
|
|
2560
|
+
result.push({
|
|
2561
|
+
seatId: metadata.seatId,
|
|
2562
|
+
membershipId: metadata.membershipId,
|
|
2563
|
+
subscriptionItemId: item.id,
|
|
2564
|
+
priceId: price,
|
|
2565
|
+
...Number.isSafeInteger(item.current_period_end) ? { currentPeriodEnd: Number(item.current_period_end) } : {}
|
|
2566
|
+
});
|
|
2567
|
+
}
|
|
2568
|
+
return result;
|
|
2569
|
+
}
|
|
2570
|
+
function projectCommercialSeats(payments, subscriptions, refunds) {
|
|
2571
|
+
return payments.map((payment) => {
|
|
2572
|
+
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 })) : []);
|
|
2573
|
+
const match = matches2.length === 1 ? matches2[0] : void 0;
|
|
2574
|
+
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);
|
|
2575
|
+
return {
|
|
2576
|
+
...payment,
|
|
2577
|
+
objectKind: "seat",
|
|
2578
|
+
objectId: payment.seatId,
|
|
2579
|
+
refundedCents,
|
|
2580
|
+
...refundedCents >= Number(payment.amountReceivedCents) && refundedCents > 0 ? { status: "refunded" } : {},
|
|
2581
|
+
...match ? {
|
|
2582
|
+
subscriptionId: match.subscription.subscriptionId,
|
|
2583
|
+
subscriptionItemId: match.item.subscriptionItemId,
|
|
2584
|
+
priceId: match.item.priceId,
|
|
2585
|
+
currentPeriodEnd: match.item.currentPeriodEnd ?? match.subscription.currentPeriodEnd,
|
|
2586
|
+
cancelAtPeriodEnd: match.subscription.cancelAtPeriodEnd
|
|
2587
|
+
} : {}
|
|
2588
|
+
};
|
|
2589
|
+
});
|
|
2590
|
+
}
|
|
2591
|
+
|
|
2485
2592
|
// src/subscription-billing-terms.ts
|
|
2486
2593
|
var import_stripe = require("@odla-ai/stripe");
|
|
2487
2594
|
|
|
@@ -2653,6 +2760,7 @@ function normalizeNetworkCommercialStripeEvent(input, digest) {
|
|
|
2653
2760
|
...chargeId ? { chargeId } : {},
|
|
2654
2761
|
...paymentIntentId ? { paymentIntentId } : {},
|
|
2655
2762
|
...price,
|
|
2763
|
+
...objectKind === "subscription" && commercialSeatItems(object2) ? { seatSubscriptionItems: commercialSeatItems(object2) } : {},
|
|
2656
2764
|
...status2 ? { status: status2 } : {},
|
|
2657
2765
|
...currency ? { currency } : {},
|
|
2658
2766
|
...amountCents !== void 0 ? { amountCents } : {},
|
|
@@ -2674,8 +2782,8 @@ function normalizeNetworkCommercialStripeEvent(input, digest) {
|
|
|
2674
2782
|
var identityFields = ["appId", "chapterId", "environment", "dataEnvironment", "runtime"];
|
|
2675
2783
|
function directSource(fact, sources) {
|
|
2676
2784
|
const mode = fact.livemode ? "live" : "test";
|
|
2677
|
-
const supplied = identityFields.filter((
|
|
2678
|
-
const candidates = sources.filter((source) => source.stripeMode === mode && supplied.every((
|
|
2785
|
+
const supplied = identityFields.filter((field2) => fact.identity[field2] !== void 0);
|
|
2786
|
+
const candidates = sources.filter((source) => source.stripeMode === mode && supplied.every((field2) => source[field2] === fact.identity[field2]));
|
|
2679
2787
|
if (supplied.length === identityFields.length) return candidates.length === 1 ? candidates[0] : "wrong";
|
|
2680
2788
|
if (supplied.length > 0 && candidates.length === 0) return "wrong";
|
|
2681
2789
|
return null;
|
|
@@ -2771,11 +2879,11 @@ function projectNetworkCommercialEvents(input, sources) {
|
|
|
2771
2879
|
}
|
|
2772
2880
|
}
|
|
2773
2881
|
const totalsMap = /* @__PURE__ */ new Map();
|
|
2774
|
-
const add = (row2,
|
|
2882
|
+
const add = (row2, field2, amount) => {
|
|
2775
2883
|
if (!row2.currency) return;
|
|
2776
2884
|
const key3 = `${row2.sourceKey}:${row2.currency}`;
|
|
2777
2885
|
const total = totalsMap.get(key3) ?? { sourceKey: row2.sourceKey, currency: row2.currency, paidCents: 0, refundedCents: 0, netCents: 0 };
|
|
2778
|
-
total[
|
|
2886
|
+
total[field2] += amount;
|
|
2779
2887
|
total.netCents = total.paidCents - total.refundedCents;
|
|
2780
2888
|
totalsMap.set(key3, total);
|
|
2781
2889
|
};
|
|
@@ -2783,7 +2891,7 @@ function projectNetworkCommercialEvents(input, sources) {
|
|
|
2783
2891
|
for (const row2 of seatPayments) add(row2, "paidCents", row2.amountReceivedCents ?? 0);
|
|
2784
2892
|
for (const row2 of refunds) add(row2, "refundedCents", row2.amountCents ?? 0);
|
|
2785
2893
|
return {
|
|
2786
|
-
...seatPayments.length ? { seatPayments } : {},
|
|
2894
|
+
...seatPayments.length ? { seatPayments, seats: projectCommercialSeats(seatPayments, subscriptions, refunds) } : {},
|
|
2787
2895
|
customers,
|
|
2788
2896
|
subscriptions,
|
|
2789
2897
|
invoices,
|
|
@@ -3357,8 +3465,8 @@ function createChapterIntegration(chapter, options = {}) {
|
|
|
3357
3465
|
}
|
|
3358
3466
|
|
|
3359
3467
|
// src/membership-policy.ts
|
|
3360
|
-
var fail2 = (
|
|
3361
|
-
throw new TypeError(`invalid membership policy: ${
|
|
3468
|
+
var fail2 = (field2) => {
|
|
3469
|
+
throw new TypeError(`invalid membership policy: ${field2}`);
|
|
3362
3470
|
};
|
|
3363
3471
|
var object = (value) => {
|
|
3364
3472
|
if (!value || typeof value !== "object" || Array.isArray(value)) return fail2("object");
|
|
@@ -3555,7 +3663,8 @@ async function issueMembershipCommercialOffer(db, input) {
|
|
|
3555
3663
|
const offerings = rows3.membershipOfferingVersion.map((row3) => membershipOfferingVersion(row3.snapshot)).filter((offer) => offer.chapterId === input.chapterId && offer.baseSkuId === input.skuId && offer.status === "published" && input.now >= offer.effectiveFrom && (offer.effectiveUntil === void 0 || input.now < offer.effectiveUntil));
|
|
3556
3664
|
const policies = rows3.discountPolicyVersion.map((row3) => membershipDiscountPolicyVersion(row3.snapshot)).filter((policy2) => policy2.chapterId === input.chapterId && policy2.status === "published" && policy2.cohortEligibleSkuIds.includes(input.skuId) && input.now >= policy2.qualifyingStartsAt && input.now < policy2.qualifyingEndsAt);
|
|
3557
3665
|
const offering = offerings.sort((a, b) => b.version - a.version)[0];
|
|
3558
|
-
const
|
|
3666
|
+
const restart = await load(db, "membershipRestart", (await membershipOfferIdForRequest({ ...input, requestId: "checkout_binding" })).replace(/^offer_/, "membership_"));
|
|
3667
|
+
const policy = restart ? void 0 : policies.sort((a, b) => b.version - a.version)[0];
|
|
3559
3668
|
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");
|
|
3560
3669
|
const approvedFoundingCount = policy ? state.cohortCounts[policy.cohortLabel] ?? 0 : 0;
|
|
3561
3670
|
if (!Number.isSafeInteger(approvedFoundingCount) || Number(approvedFoundingCount) < 0) reject("invalid_cohort_count");
|
|
@@ -3593,8 +3702,73 @@ async function issueMembershipCommercialOffer(db, input) {
|
|
|
3593
3702
|
return restore(stored, input);
|
|
3594
3703
|
}
|
|
3595
3704
|
|
|
3596
|
-
// src/membership-authority
|
|
3597
|
-
var
|
|
3705
|
+
// src/membership-cancel-authority.ts
|
|
3706
|
+
var import_stripe2 = require("@odla-ai/stripe");
|
|
3707
|
+
|
|
3708
|
+
// src/commercial-evidence-guard.ts
|
|
3709
|
+
async function commercialEvidenceGuard(db, eventId, digest, code = "membership_payment_evidence_quarantined") {
|
|
3710
|
+
const ns = "networkCommercialExceptions", id10 = `network-commercial-exception:${eventId}:collision`;
|
|
3711
|
+
const row2 = (await db.query({ [ns]: { $: { where: { id: id10 }, limit: 1 } } }))[ns]?.[0];
|
|
3712
|
+
if (!row2) return { ns, id: id10, exists: false };
|
|
3713
|
+
if (row2.status !== "resolved" || row2.resolvedDigest !== digest || typeof digest !== "string") {
|
|
3714
|
+
throw Object.assign(new Error(code), { code });
|
|
3715
|
+
}
|
|
3716
|
+
return { ns, id: id10, exists: true, equals: { status: "resolved", resolvedDigest: digest, updatedAt: row2.updatedAt } };
|
|
3717
|
+
}
|
|
3718
|
+
|
|
3719
|
+
// src/membership-authority-state.ts
|
|
3720
|
+
var membershipFail = (code) => {
|
|
3721
|
+
throw Object.assign(new Error(code), { code });
|
|
3722
|
+
};
|
|
3723
|
+
var membershipLoad = async (db, ns, id10) => (await db.query({ [ns]: { $: { where: { id: id10 }, limit: 1 } } }))[ns]?.[0];
|
|
3724
|
+
var membershipScopeKeys = ["appId", "chapterId", "environment", "runtime", "applicationId", "skuId"];
|
|
3725
|
+
var membershipScope = (input) => Object.fromEntries(membershipScopeKeys.map((key3) => [key3, input[key3]]));
|
|
3726
|
+
function requireMembershipScope(row2, input) {
|
|
3727
|
+
if (!row2 || membershipScopeKeys.some((key3) => row2[key3] !== input[key3])) membershipFail("membership_scope_mismatch");
|
|
3728
|
+
}
|
|
3729
|
+
async function loadMembershipAuthorityState(db, input) {
|
|
3730
|
+
const row2 = await membershipLoad(db, "membershipAuthorityState", `${input.chapterId}:${input.environment}`);
|
|
3731
|
+
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");
|
|
3732
|
+
return row2;
|
|
3733
|
+
}
|
|
3734
|
+
var membershipUpdate = (ns, id10, attrs) => ({ t: "update", ns, id: id10, attrs });
|
|
3735
|
+
var membershipClock = (now) => {
|
|
3736
|
+
if (!Number.isSafeInteger(now) || now < 0) membershipFail("invalid_membership_clock");
|
|
3737
|
+
};
|
|
3738
|
+
|
|
3739
|
+
// src/membership-restart-payment.ts
|
|
3740
|
+
async function membershipRestartPayment(db, target, member, now) {
|
|
3741
|
+
const id10 = String(member.id), restart = await membershipLoad(db, "membershipRestart", id10);
|
|
3742
|
+
requireMembershipScope(restart, target);
|
|
3743
|
+
const entitlement = await membershipLoad(db, "networkEntitlement", `${id10}:primary`);
|
|
3744
|
+
requireMembershipScope(entitlement, target);
|
|
3745
|
+
if (entitlement.membershipId !== id10 || entitlement.kind !== "primary") membershipFail("membership_restart_conflict");
|
|
3746
|
+
if (restart.status !== "ready" || member.status !== "restarting" || member.restartId !== restart.restartId || member.memberNumber !== restart.memberNumber || !Number.isSafeInteger(member.journeyApprovedAt)) membershipFail("membership_restart_conflict");
|
|
3747
|
+
return {
|
|
3748
|
+
restartId: String(restart.restartId),
|
|
3749
|
+
entitlementAttrs: { revision: Number(entitlement.revision) + 1, createdAt: entitlement.createdAt },
|
|
3750
|
+
attrs: {
|
|
3751
|
+
memberNumber: member.memberNumber,
|
|
3752
|
+
journeyApprovedAt: member.journeyApprovedAt,
|
|
3753
|
+
founding: member.founding,
|
|
3754
|
+
cohort: member.cohort,
|
|
3755
|
+
...member.personId ? { personId: member.personId } : {},
|
|
3756
|
+
status: "active",
|
|
3757
|
+
revision: Number(member.revision) + 1,
|
|
3758
|
+
createdAt: member.createdAt,
|
|
3759
|
+
graceEndsAt: 0
|
|
3760
|
+
},
|
|
3761
|
+
ops: [
|
|
3762
|
+
membershipUpdate("membershipRestart", id10, { status: "complete" }),
|
|
3763
|
+
{ t: "retract", ns: "networkMembership", id: id10, attrs: ["refundRequestedAt", "cancellationRequestedAt", "discountPolicyVersionId", "discountSnapshot"] }
|
|
3764
|
+
],
|
|
3765
|
+
guards: [
|
|
3766
|
+
{ ns: "networkMembership", id: id10, exists: true, equals: { status: "restarting", revision: member.revision, restartId: restart.restartId } },
|
|
3767
|
+
{ ns: "membershipRestart", id: id10, exists: true, equals: { status: "ready", restartId: restart.restartId } },
|
|
3768
|
+
{ ns: "networkEntitlement", id: String(entitlement.id), exists: true, equals: { revision: entitlement.revision, status: entitlement.status } }
|
|
3769
|
+
]
|
|
3770
|
+
};
|
|
3771
|
+
}
|
|
3598
3772
|
|
|
3599
3773
|
// src/membership-checkout-binding.ts
|
|
3600
3774
|
var keys = ["appId", "chapterId", "environment", "runtime", "applicationId", "skuId", "offerId", "offerDigest", "quoteDigest", "priceId"];
|
|
@@ -3621,10 +3795,11 @@ async function bindMembershipCheckout(db, input, now) {
|
|
|
3621
3795
|
const row2 = await load2(db, "membershipCheckoutOffer", input.offerId);
|
|
3622
3796
|
const offer = await verifyIssuedMembershipOffer(row2, { ...input, now, lifetimeMs: 6e4 });
|
|
3623
3797
|
if (offer.digest !== input.offerDigest || offer.expiresAt <= now || offer.issuedAt > now) return reject2("membership_offer_expired");
|
|
3798
|
+
const restart = await load2(db, "membershipRestart", id10.replace(/^checkout_/, "membership_"));
|
|
3624
3799
|
const receipt = { id: id10, ...Object.fromEntries(keys.map((key3) => [key3, input[key3]])), boundAt: now };
|
|
3625
3800
|
try {
|
|
3626
3801
|
await db.transact([{ t: "update", ns: "membershipOfferCheckout", id: id10, attrs: receipt }], {
|
|
3627
|
-
mutationId: `membership-checkout:${id10}`,
|
|
3802
|
+
mutationId: `membership-checkout:${id10}${restart?.restartId ? `:${restart.restartId}` : ""}`,
|
|
3628
3803
|
guards: [
|
|
3629
3804
|
{ ns: "membershipOfferCheckout", id: id10, exists: false },
|
|
3630
3805
|
{ ns: "membershipCheckoutOffer", id: input.offerId, exists: true, equals: { digest: input.offerDigest } },
|
|
@@ -3639,26 +3814,6 @@ async function bindMembershipCheckout(db, input, now) {
|
|
|
3639
3814
|
return verifyMembershipCheckoutBinding(saved, input);
|
|
3640
3815
|
}
|
|
3641
3816
|
|
|
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
3817
|
// src/membership-payment-authority.ts
|
|
3663
3818
|
async function networkMembershipId(input) {
|
|
3664
3819
|
return (await membershipCheckoutBindingId(input)).replace(/^checkout_/, "membership_");
|
|
@@ -3676,9 +3831,7 @@ async function confirmMembershipPayment(db, input, now) {
|
|
|
3676
3831
|
if (binding.offerDigest !== offer.digest) membershipFail("membership_checkout_conflict");
|
|
3677
3832
|
const eventRow = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
|
|
3678
3833
|
if (!eventRow) membershipFail("membership_payment_evidence_pending");
|
|
3679
|
-
|
|
3680
|
-
membershipFail("membership_payment_evidence_quarantined");
|
|
3681
|
-
}
|
|
3834
|
+
const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, eventRow.digest);
|
|
3682
3835
|
const fact = eventRow.fact;
|
|
3683
3836
|
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
3837
|
appId: input.appId,
|
|
@@ -3694,8 +3847,9 @@ async function confirmMembershipPayment(db, input, now) {
|
|
|
3694
3847
|
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
3848
|
const id10 = await networkMembershipId(input), entitlementId = `${id10}:primary`;
|
|
3696
3849
|
let member = await membershipLoad(db, "networkMembership", id10);
|
|
3697
|
-
if (!member) {
|
|
3850
|
+
if (!member || member.status === "restarting") {
|
|
3698
3851
|
const offering = offer.contract.offering;
|
|
3852
|
+
const restart = member ? await membershipRestartPayment(db, input, member, now) : null;
|
|
3699
3853
|
const attrs = {
|
|
3700
3854
|
id: id10,
|
|
3701
3855
|
membershipId: id10,
|
|
@@ -3727,7 +3881,8 @@ async function confirmMembershipPayment(db, input, now) {
|
|
|
3727
3881
|
refundedCents: 0,
|
|
3728
3882
|
revision: 1,
|
|
3729
3883
|
createdAt: now,
|
|
3730
|
-
updatedAt: now
|
|
3884
|
+
updatedAt: now,
|
|
3885
|
+
...restart?.attrs
|
|
3731
3886
|
};
|
|
3732
3887
|
try {
|
|
3733
3888
|
await db.transact([
|
|
@@ -3737,21 +3892,23 @@ async function confirmMembershipPayment(db, input, now) {
|
|
|
3737
3892
|
...membershipScope(input),
|
|
3738
3893
|
membershipId: id10,
|
|
3739
3894
|
kind: "primary",
|
|
3740
|
-
status: "pending_approval",
|
|
3895
|
+
status: restart?.attrs.status === "active" ? "active" : "pending_approval",
|
|
3741
3896
|
termStartedAt: paidAt,
|
|
3742
3897
|
termEndsAt: endsAt,
|
|
3743
3898
|
offeringSnapshot: offering,
|
|
3744
3899
|
sourceEventId: input.eventId,
|
|
3745
3900
|
revision: 1,
|
|
3746
3901
|
createdAt: now,
|
|
3747
|
-
updatedAt: now
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3902
|
+
updatedAt: now,
|
|
3903
|
+
...restart?.entitlementAttrs
|
|
3904
|
+
}),
|
|
3905
|
+
...restart?.ops ?? []
|
|
3906
|
+
], { mutationId: `membership-paid:${id10}${restart ? `:${restart.restartId}` : ""}`, guards: [
|
|
3907
|
+
...restart?.guards ?? [{ ns: "networkMembership", id: id10, exists: false }],
|
|
3751
3908
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
|
|
3752
3909
|
{ ns: "membershipOfferCheckout", id: String(binding.id), exists: true, equals: { offerDigest: offer.digest } },
|
|
3753
3910
|
{ ns: "networkCommercialEvents", id: String(eventRow.id), exists: true, equals: { digest: eventRow.digest } },
|
|
3754
|
-
|
|
3911
|
+
evidenceGuard
|
|
3755
3912
|
] });
|
|
3756
3913
|
} catch (error) {
|
|
3757
3914
|
if (error.code !== "transact_guard_failed") throw error;
|
|
@@ -3770,6 +3927,57 @@ async function confirmMembershipPayment(db, input, now) {
|
|
|
3770
3927
|
};
|
|
3771
3928
|
}
|
|
3772
3929
|
|
|
3930
|
+
// src/membership-cancel-authority.ts
|
|
3931
|
+
async function cancelMembershipAtTermEnd(db, target, now, stripeSecret) {
|
|
3932
|
+
membershipClock(now);
|
|
3933
|
+
const id10 = await networkMembershipId(target);
|
|
3934
|
+
const state = await loadMembershipAuthorityState(db, target);
|
|
3935
|
+
const member = await membershipLoad(db, "networkMembership", id10);
|
|
3936
|
+
requireMembershipScope(member, target);
|
|
3937
|
+
if (!["active", "cancel_at_term_end", "payment_grace", "paid_pending_approval", "lapsed"].includes(String(member.status))) {
|
|
3938
|
+
membershipFail("membership_cancellation_unavailable");
|
|
3939
|
+
}
|
|
3940
|
+
const path = `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`;
|
|
3941
|
+
const read2 = await (0, import_stripe2.stripeRequest)({ secretKey: stripeSecret }, "GET", path);
|
|
3942
|
+
const sub = read2.body;
|
|
3943
|
+
if (!read2.ok || sub.id !== member.providerSubscriptionId || sub.customer !== member.providerCustomerId || sub.livemode !== (target.environment === "prod") || !Object.entries({
|
|
3944
|
+
...stripePartitionMetadata(target),
|
|
3945
|
+
applicationId: target.applicationId
|
|
3946
|
+
}).every(([key3, value]) => sub.metadata?.[key3] === value)) membershipFail("membership_cancellation_provider_mismatch");
|
|
3947
|
+
if (sub.status !== "canceled" && sub.cancel_at_period_end !== true) {
|
|
3948
|
+
const write = await (0, import_stripe2.stripeRequest)(
|
|
3949
|
+
{ secretKey: stripeSecret },
|
|
3950
|
+
"POST",
|
|
3951
|
+
path,
|
|
3952
|
+
{ cancel_at_period_end: true, proration_behavior: "none" },
|
|
3953
|
+
{ idempotencyKey: `membership-cancel-at-term:${id10}:${member.providerSubscriptionId}` }
|
|
3954
|
+
);
|
|
3955
|
+
if (!write.ok || write.body.id !== member.providerSubscriptionId || write.body.cancel_at_period_end !== true) {
|
|
3956
|
+
membershipFail("membership_cancellation_pending");
|
|
3957
|
+
}
|
|
3958
|
+
}
|
|
3959
|
+
const status2 = Number(member.termEndsAt) <= now ? "lapsed" : member.journeyApprovedAt ? "cancel_at_term_end" : "paid_pending_approval";
|
|
3960
|
+
if (member.status !== status2 || member.autoRenew !== false || Number(member.graceEndsAt ?? 0) !== 0) {
|
|
3961
|
+
await db.transact([membershipUpdate("networkMembership", id10, {
|
|
3962
|
+
status: status2,
|
|
3963
|
+
autoRenew: false,
|
|
3964
|
+
graceEndsAt: 0,
|
|
3965
|
+
cancellationRequestedAt: now,
|
|
3966
|
+
revision: Number(member.revision) + 1,
|
|
3967
|
+
updatedAt: now
|
|
3968
|
+
})], {
|
|
3969
|
+
guards: [
|
|
3970
|
+
{ ns: "networkMembership", id: id10, exists: true, equals: { revision: member.revision } },
|
|
3971
|
+
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { revision: state.revision, mode: "authoritative" } }
|
|
3972
|
+
]
|
|
3973
|
+
});
|
|
3974
|
+
}
|
|
3975
|
+
return { membershipId: id10, status: status2, termEndsAt: Number(member.termEndsAt), graceEndsAt: 0, autoRenew: false };
|
|
3976
|
+
}
|
|
3977
|
+
|
|
3978
|
+
// src/membership-authority-handler.ts
|
|
3979
|
+
var import_db = require("@odla-ai/db");
|
|
3980
|
+
|
|
3773
3981
|
// src/membership-seat-activation.ts
|
|
3774
3982
|
async function namedSeatPrimaryActivation(db, member, now) {
|
|
3775
3983
|
if (!member.namedSeatId) return { ops: [], guards: [] };
|
|
@@ -3862,7 +4070,7 @@ async function approveNetworkMembership(db, input, now) {
|
|
|
3862
4070
|
}
|
|
3863
4071
|
|
|
3864
4072
|
// src/payments-refund.ts
|
|
3865
|
-
var
|
|
4073
|
+
var import_stripe3 = require("@odla-ai/stripe");
|
|
3866
4074
|
var row = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
3867
4075
|
var id7 = (value) => typeof value === "string" ? value : String(row(value).id ?? "");
|
|
3868
4076
|
var providerId2 = (value, prefix) => new RegExp(`^${prefix}_[A-Za-z0-9]+$`).test(value);
|
|
@@ -3872,7 +4080,7 @@ function fail3(code = "refund_target_mismatch") {
|
|
|
3872
4080
|
async function refundSubscriptionPayment(secretKey, input) {
|
|
3873
4081
|
if (!providerId2(input.customerId, "cus") || !providerId2(input.subscriptionId, "sub")) fail3();
|
|
3874
4082
|
const call = async (method, path, params, key3) => {
|
|
3875
|
-
const result = await (0,
|
|
4083
|
+
const result = await (0, import_stripe3.stripeRequest)({ secretKey }, method, path, params, key3 ? { idempotencyKey: key3 } : {});
|
|
3876
4084
|
if (!result.ok) fail3("stripe_error");
|
|
3877
4085
|
if (!result.body || typeof result.body !== "object" || Array.isArray(result.body)) fail3("stripe_invalid_response");
|
|
3878
4086
|
return result.body;
|
|
@@ -4043,12 +4251,13 @@ async function refundNetworkMembership(db, input, secretKey, now) {
|
|
|
4043
4251
|
}
|
|
4044
4252
|
|
|
4045
4253
|
// src/membership-provider-lifecycle.ts
|
|
4046
|
-
var
|
|
4254
|
+
var import_stripe4 = require("@odla-ai/stripe");
|
|
4047
4255
|
|
|
4048
4256
|
// src/membership-renewal-authority.ts
|
|
4049
4257
|
async function reconcileMembershipRenewal(db, input, now) {
|
|
4050
4258
|
membershipClock(now);
|
|
4051
4259
|
const id10 = await networkMembershipId(input), eventRow = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
|
|
4260
|
+
const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, eventRow?.digest, "membership_renewal_evidence_pending");
|
|
4052
4261
|
const fact = eventRow?.fact;
|
|
4053
4262
|
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
4263
|
appId: input.appId,
|
|
@@ -4056,7 +4265,7 @@ async function reconcileMembershipRenewal(db, input, now) {
|
|
|
4056
4265
|
environment: input.environment,
|
|
4057
4266
|
dataEnvironment: input.environment,
|
|
4058
4267
|
runtime: input.runtime
|
|
4059
|
-
}).some(([key3, value]) => fact.identity[key3] !== value)
|
|
4268
|
+
}).some(([key3, value]) => fact.identity[key3] !== value)) {
|
|
4060
4269
|
return membershipFail("membership_renewal_evidence_pending");
|
|
4061
4270
|
}
|
|
4062
4271
|
for (let attempt = 0; attempt < 8; attempt++) {
|
|
@@ -4070,7 +4279,7 @@ async function reconcileMembershipRenewal(db, input, now) {
|
|
|
4070
4279
|
autoRenew: member.autoRenew === true
|
|
4071
4280
|
});
|
|
4072
4281
|
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();
|
|
4282
|
+
if (["refunded", "refund_pending", "canceled", "declined", "restarting", "superseded"].includes(String(member.status))) return receipt();
|
|
4074
4283
|
const lines = fact.recurringInvoiceLines;
|
|
4075
4284
|
if (!lines?.length || lines.length > 2) membershipFail("membership_renewal_lines_missing");
|
|
4076
4285
|
const start = lines[0].startsAt, end = lines[0].endsAt;
|
|
@@ -4079,7 +4288,7 @@ async function reconcileMembershipRenewal(db, input, now) {
|
|
|
4079
4288
|
if (start !== member.termEndsAt || start > now + 3e4) membershipFail("membership_renewal_period_gap");
|
|
4080
4289
|
const seat = member.namedSeatId ? await membershipLoad(db, "networkSeat", String(member.namedSeatId)) : void 0;
|
|
4081
4290
|
if (seat) requireMembershipScope(seat, input);
|
|
4082
|
-
const billedSeat = seat && seat.providerSubscriptionItemId && !["refunded", "refund_pending"].includes(String(seat.status)) ? seat : void 0;
|
|
4291
|
+
const billedSeat = seat && seat.providerSubscriptionId === member.providerSubscriptionId && seat.providerSubscriptionItemId && !["refunded", "refund_pending"].includes(String(seat.status)) ? seat : void 0;
|
|
4083
4292
|
const prices = [String(member.providerPriceId), ...billedSeat ? [String(billedSeat.priceId)] : []].sort();
|
|
4084
4293
|
const amount = Number(member.renewalAmountCents) + Number(billedSeat?.amountCents ?? 0);
|
|
4085
4294
|
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 +4321,7 @@ async function reconcileMembershipRenewal(db, input, now) {
|
|
|
4112
4321
|
{ ns: "networkEntitlement", id: `${id10}:primary`, exists: true, equals: { revision: entitlement.revision } },
|
|
4113
4322
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { revision: state.revision, mode: "authoritative" } },
|
|
4114
4323
|
{ ns: "networkCommercialEvents", id: String(eventRow.id), exists: true, equals: { digest: eventRow.digest } },
|
|
4115
|
-
|
|
4324
|
+
evidenceGuard
|
|
4116
4325
|
];
|
|
4117
4326
|
if (seat) guards.push({ ns: "networkSeat", id: String(seat.id), exists: true, equals: { revision: seat.revision } });
|
|
4118
4327
|
if (billedSeat?.entitlementId) {
|
|
@@ -4158,14 +4367,14 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
4158
4367
|
const row2 = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
|
|
4159
4368
|
const fact = row2?.fact;
|
|
4160
4369
|
if (fact?.objectKind === "invoice") return reconcileMembershipRenewal(db, input, now);
|
|
4161
|
-
const
|
|
4370
|
+
const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, row2?.digest, "membership_billing_evidence_pending");
|
|
4162
4371
|
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
4372
|
appId: input.appId,
|
|
4164
4373
|
chapterId: input.chapterId,
|
|
4165
4374
|
environment: input.environment,
|
|
4166
4375
|
dataEnvironment: input.environment,
|
|
4167
4376
|
runtime: input.runtime
|
|
4168
|
-
}).some(([key3, value]) => fact.identity[key3] !== value)
|
|
4377
|
+
}).some(([key3, value]) => fact.identity[key3] !== value)) membershipFail("membership_billing_evidence_pending");
|
|
4169
4378
|
const sk = await getVaultSecret(db, stripeSecretName);
|
|
4170
4379
|
if (!sk) membershipFail("membership_billing_provider_unavailable");
|
|
4171
4380
|
const id10 = await networkMembershipId(input);
|
|
@@ -4179,6 +4388,7 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
4179
4388
|
graceEndsAt: Number(value.graceEndsAt ?? 0),
|
|
4180
4389
|
autoRenew: value.autoRenew === true
|
|
4181
4390
|
});
|
|
4391
|
+
if (["restarting"].includes(String(member.status))) return receipt();
|
|
4182
4392
|
if (fact.customerId !== member.providerCustomerId) membershipFail("membership_billing_scope_mismatch");
|
|
4183
4393
|
if (fact.eventType === "charge.refunded") {
|
|
4184
4394
|
if (!fact.chargeId || fact.seatId || fact.amountCents !== member.amountCents || fact.currency !== member.currency) membershipFail("membership_refund_evidence_mismatch");
|
|
@@ -4191,15 +4401,15 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
4191
4401
|
alreadyRefundedChargeId: fact.chargeId,
|
|
4192
4402
|
cancelSubscription: false
|
|
4193
4403
|
});
|
|
4194
|
-
|
|
4404
|
+
await commercialEvidenceGuard(db, input.eventId, row2?.digest, "membership_billing_evidence_pending");
|
|
4195
4405
|
await refundNetworkMembership(db, input, sk, now);
|
|
4196
4406
|
const saved2 = await membershipLoad(db, "networkMembership", id10);
|
|
4197
4407
|
requireMembershipScope(saved2, input);
|
|
4198
4408
|
return receipt(saved2);
|
|
4199
4409
|
}
|
|
4200
4410
|
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,
|
|
4411
|
+
if (["refund_pending", "refunded", "declined", "canceled", "restarting"].includes(String(member.status))) return receipt();
|
|
4412
|
+
const result = await (0, import_stripe4.stripeRequest)({ secretKey: sk }, "GET", `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`);
|
|
4203
4413
|
const sub = result.body;
|
|
4204
4414
|
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
4415
|
membershipFail("membership_billing_provider_mismatch");
|
|
@@ -4223,7 +4433,7 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
4223
4433
|
{ ns: "networkEntitlement", id: String(entitlement.id), exists: true, equals: { revision: entitlement.revision } },
|
|
4224
4434
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
|
|
4225
4435
|
{ ns: "networkCommercialEvents", id: String(row2.id), exists: true, equals: { digest: fact.digest } },
|
|
4226
|
-
|
|
4436
|
+
evidenceGuard
|
|
4227
4437
|
] });
|
|
4228
4438
|
} catch (error) {
|
|
4229
4439
|
if (error.code !== "transact_guard_failed") throw error;
|
|
@@ -4236,6 +4446,120 @@ async function reconcileMembershipBilling(db, input, now, stripeSecretName = "st
|
|
|
4236
4446
|
return membershipFail("membership_billing_retry_required");
|
|
4237
4447
|
}
|
|
4238
4448
|
|
|
4449
|
+
// src/membership-restart-seat.ts
|
|
4450
|
+
async function expireRestartedMembershipSeat(db, target, member, now) {
|
|
4451
|
+
const ops = [], guards = [];
|
|
4452
|
+
if (!member.namedSeatId) return { ops, guards };
|
|
4453
|
+
const seat = await membershipLoad(db, "networkSeat", String(member.namedSeatId));
|
|
4454
|
+
requireMembershipScope(seat, target);
|
|
4455
|
+
if (!["refunded", "refund_pending", "expired"].includes(String(seat.status))) {
|
|
4456
|
+
ops.push(membershipUpdate("networkSeat", String(seat.id), { status: "expired", revision: Number(seat.revision) + 1, updatedAt: now }));
|
|
4457
|
+
guards.push({ ns: "networkSeat", id: String(seat.id), exists: true, equals: { revision: seat.revision } });
|
|
4458
|
+
}
|
|
4459
|
+
if (seat.entitlementId) {
|
|
4460
|
+
const entitlement = await membershipLoad(db, "networkEntitlement", String(seat.entitlementId));
|
|
4461
|
+
requireMembershipScope(entitlement, target);
|
|
4462
|
+
ops.push(membershipUpdate("networkEntitlement", String(entitlement.id), { status: "closed", revision: Number(entitlement.revision) + 1, updatedAt: now }));
|
|
4463
|
+
guards.push({ ns: "networkEntitlement", id: String(entitlement.id), exists: true, equals: { revision: entitlement.revision } });
|
|
4464
|
+
}
|
|
4465
|
+
return { ops, guards };
|
|
4466
|
+
}
|
|
4467
|
+
|
|
4468
|
+
// src/membership-restart-authority.ts
|
|
4469
|
+
var import_stripe5 = require("@odla-ai/stripe");
|
|
4470
|
+
|
|
4471
|
+
// src/stripe-event-digest.ts
|
|
4472
|
+
async function stripePayloadDigest(value) {
|
|
4473
|
+
const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
|
|
4474
|
+
return [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
4475
|
+
}
|
|
4476
|
+
|
|
4477
|
+
// src/membership-restart-authority.ts
|
|
4478
|
+
function membershipCanRestart(member, now) {
|
|
4479
|
+
const end = member.status === "payment_grace" ? Number(member.graceEndsAt) : Number(member.termEndsAt);
|
|
4480
|
+
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);
|
|
4481
|
+
}
|
|
4482
|
+
async function stopPreviousSubscription(member, target, secret) {
|
|
4483
|
+
const path = `/v1/subscriptions/${encodeURIComponent(String(member.providerSubscriptionId))}`;
|
|
4484
|
+
const result = await (0, import_stripe5.stripeRequest)({ secretKey: secret }, "GET", path);
|
|
4485
|
+
const sub = result.body;
|
|
4486
|
+
if (!result.ok || sub.id !== member.providerSubscriptionId || sub.customer !== member.providerCustomerId || sub.livemode !== (target.environment === "prod") || !Object.entries({
|
|
4487
|
+
...stripePartitionMetadata(target),
|
|
4488
|
+
applicationId: target.applicationId
|
|
4489
|
+
}).every(([key3, value]) => sub.metadata?.[key3] === value)) membershipFail("membership_restart_provider_mismatch");
|
|
4490
|
+
if (sub.status === "canceled") return;
|
|
4491
|
+
const canceled = await (0, import_stripe5.stripeRequest)(
|
|
4492
|
+
{ secretKey: secret },
|
|
4493
|
+
"DELETE",
|
|
4494
|
+
path,
|
|
4495
|
+
{ invoice_now: false, prorate: false },
|
|
4496
|
+
{ idempotencyKey: `membership-restart-cancel:${member.providerSubscriptionId}` }
|
|
4497
|
+
);
|
|
4498
|
+
if (!canceled.ok || canceled.body.id !== member.providerSubscriptionId || canceled.body.status !== "canceled") membershipFail("membership_restart_cancellation_pending");
|
|
4499
|
+
}
|
|
4500
|
+
async function prepareMembershipRestart(db, input, now, stripeSecret) {
|
|
4501
|
+
membershipClock(now);
|
|
4502
|
+
if (!/^sub_[A-Za-z0-9_]+$/.test(input.previousSubscriptionId)) membershipFail("membership_restart_conflict");
|
|
4503
|
+
const id10 = await networkMembershipId(input), bindingId = await membershipCheckoutBindingId(input);
|
|
4504
|
+
const restartId = `restart_${await stripePayloadDigest(JSON.stringify([id10, input.previousSubscriptionId]))}`;
|
|
4505
|
+
const state = await loadMembershipAuthorityState(db, input);
|
|
4506
|
+
const previous = await membershipLoad(db, "networkMembership", id10);
|
|
4507
|
+
requireMembershipScope(previous, input);
|
|
4508
|
+
const receipt = {
|
|
4509
|
+
applicationId: input.applicationId,
|
|
4510
|
+
membershipId: id10,
|
|
4511
|
+
restartId,
|
|
4512
|
+
memberNumber: Number(previous.memberNumber),
|
|
4513
|
+
status: "ready"
|
|
4514
|
+
};
|
|
4515
|
+
const existing = await membershipLoad(db, "membershipRestart", id10);
|
|
4516
|
+
if (existing?.restartId === restartId && existing.status === "ready") {
|
|
4517
|
+
requireMembershipScope(existing, input);
|
|
4518
|
+
return receipt;
|
|
4519
|
+
}
|
|
4520
|
+
if (previous.providerSubscriptionId !== input.previousSubscriptionId) membershipFail("membership_restart_conflict");
|
|
4521
|
+
if (previous.status !== "restarting") {
|
|
4522
|
+
if (!membershipCanRestart(previous, now)) membershipFail("membership_restart_not_eligible");
|
|
4523
|
+
const binding2 = await membershipLoad(db, "membershipOfferCheckout", bindingId);
|
|
4524
|
+
requireMembershipScope(binding2, input);
|
|
4525
|
+
const seats = await expireRestartedMembershipSeat(db, input, previous, now);
|
|
4526
|
+
await db.transact([
|
|
4527
|
+
...seats.ops,
|
|
4528
|
+
membershipUpdate("membershipPurchaseHistory", restartId, {
|
|
4529
|
+
id: restartId,
|
|
4530
|
+
...membershipScope(input),
|
|
4531
|
+
membershipId: id10,
|
|
4532
|
+
providerSubscriptionId: input.previousSubscriptionId,
|
|
4533
|
+
memberSnapshot: previous,
|
|
4534
|
+
bindingSnapshot: binding2,
|
|
4535
|
+
archivedAt: now
|
|
4536
|
+
}),
|
|
4537
|
+
membershipUpdate("networkMembership", id10, { status: "restarting", restartId, revision: Number(previous.revision) + 1, updatedAt: now })
|
|
4538
|
+
], { mutationId: `membership-restart-reserve:${restartId}`, guards: [
|
|
4539
|
+
...seats.guards,
|
|
4540
|
+
{ ns: "networkMembership", id: id10, exists: true, equals: { revision: previous.revision, status: previous.status } },
|
|
4541
|
+
{ ns: "membershipPurchaseHistory", id: restartId, exists: false },
|
|
4542
|
+
{ ns: "membershipOfferCheckout", id: bindingId, exists: true, equals: { offerDigest: binding2.offerDigest } },
|
|
4543
|
+
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
|
|
4544
|
+
] });
|
|
4545
|
+
} else if (previous.restartId !== restartId) membershipFail("membership_restart_conflict");
|
|
4546
|
+
const reserved = await membershipLoad(db, "networkMembership", id10);
|
|
4547
|
+
requireMembershipScope(reserved, input);
|
|
4548
|
+
if (reserved.status !== "restarting" || reserved.restartId !== restartId) membershipFail("membership_restart_conflict");
|
|
4549
|
+
await stopPreviousSubscription(reserved, input, stripeSecret);
|
|
4550
|
+
const binding = await membershipLoad(db, "membershipOfferCheckout", bindingId);
|
|
4551
|
+
requireMembershipScope(binding, input);
|
|
4552
|
+
await db.transact([
|
|
4553
|
+
membershipUpdate("membershipRestart", id10, { id: id10, ...membershipScope(input), ...receipt, previousSubscriptionId: input.previousSubscriptionId, createdAt: now }),
|
|
4554
|
+
{ t: "delete", ns: "membershipOfferCheckout", id: bindingId }
|
|
4555
|
+
], { mutationId: `membership-restart-ready:${restartId}`, guards: [
|
|
4556
|
+
{ ns: "membershipRestart", id: id10, exists: Boolean(existing), ...existing ? { equals: { restartId: existing.restartId, status: existing.status } } : {} },
|
|
4557
|
+
{ ns: "networkMembership", id: id10, exists: true, equals: { revision: reserved.revision, status: "restarting", restartId } },
|
|
4558
|
+
{ ns: "membershipOfferCheckout", id: bindingId, exists: true, equals: { offerDigest: binding.offerDigest } }
|
|
4559
|
+
] });
|
|
4560
|
+
return receipt;
|
|
4561
|
+
}
|
|
4562
|
+
|
|
4239
4563
|
// src/membership-access-authority.ts
|
|
4240
4564
|
async function readMembershipAccess(db, input, now) {
|
|
4241
4565
|
membershipClock(now);
|
|
@@ -4252,7 +4576,8 @@ async function readMembershipAccess(db, input, now) {
|
|
|
4252
4576
|
allowed: allowed2,
|
|
4253
4577
|
status: String(member.status),
|
|
4254
4578
|
accessEndsAt: Number.isSafeInteger(endsAt) ? endsAt : 0,
|
|
4255
|
-
revision: Number(member.revision)
|
|
4579
|
+
revision: Number(member.revision),
|
|
4580
|
+
restartEligible: !allowed2 && membershipCanRestart(member, now)
|
|
4256
4581
|
};
|
|
4257
4582
|
}
|
|
4258
4583
|
async function verifyMembershipAccessReceipt(value, input, now) {
|
|
@@ -4260,7 +4585,7 @@ async function verifyMembershipAccessReceipt(value, input, now) {
|
|
|
4260
4585
|
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
4586
|
throw new Error("invalid network access receipt");
|
|
4262
4587
|
}
|
|
4263
|
-
return { membershipId: row2.membershipId, allowed: row2.allowed, status: row2.status, accessEndsAt: row2.accessEndsAt, revision: row2.revision };
|
|
4588
|
+
return { membershipId: row2.membershipId, allowed: row2.allowed, status: row2.status, accessEndsAt: row2.accessEndsAt, revision: row2.revision, ...row2.restartEligible === true && !row2.allowed ? { restartEligible: true } : {} };
|
|
4264
4589
|
}
|
|
4265
4590
|
|
|
4266
4591
|
// src/membership-seat-reservation.ts
|
|
@@ -4341,16 +4666,16 @@ async function reserveNamedMembershipSeat(db, input, now) {
|
|
|
4341
4666
|
}
|
|
4342
4667
|
|
|
4343
4668
|
// src/membership-seat-stripe.ts
|
|
4344
|
-
var
|
|
4669
|
+
var import_stripe7 = require("@odla-ai/stripe");
|
|
4345
4670
|
|
|
4346
4671
|
// src/subscription-provider-contract.ts
|
|
4347
|
-
var
|
|
4672
|
+
var import_stripe6 = require("@odla-ai/stripe");
|
|
4348
4673
|
function subscriptionContractMismatch() {
|
|
4349
4674
|
throw Object.assign(new Error("Stripe subscription does not match the checkout contract"), { code: "stripe_contract_mismatch" });
|
|
4350
4675
|
}
|
|
4351
4676
|
async function verifySubscriptionPrice(secretKey, input, resume = false) {
|
|
4352
4677
|
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,
|
|
4678
|
+
const result = await (0, import_stripe6.stripeRequest)({ secretKey }, "GET", `/v1/prices/${encodeURIComponent(input.priceId)}`);
|
|
4354
4679
|
const price = result.body;
|
|
4355
4680
|
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
4681
|
return typeof price.product === "string" ? price.product : void 0;
|
|
@@ -4368,7 +4693,7 @@ var metadataFor = (seat) => ({
|
|
|
4368
4693
|
});
|
|
4369
4694
|
var matches = (value, expected) => value && Object.entries(expected).every(([key3, item]) => value[key3] === item);
|
|
4370
4695
|
async function read(sk, path, params) {
|
|
4371
|
-
const response = await (0,
|
|
4696
|
+
const response = await (0, import_stripe7.stripeRequest)({ secretKey: sk }, "GET", path, params);
|
|
4372
4697
|
if (!response.ok) return reject3();
|
|
4373
4698
|
return response.body;
|
|
4374
4699
|
}
|
|
@@ -4405,11 +4730,11 @@ async function createNamedSeatStripePayment(sk, seat, now) {
|
|
|
4405
4730
|
}
|
|
4406
4731
|
await subscriptionForSeat(sk, seat);
|
|
4407
4732
|
const metadata = metadataFor(seat);
|
|
4408
|
-
const result = seat.providerPaymentIntentId ? await (0,
|
|
4733
|
+
const result = seat.providerPaymentIntentId ? await (0, import_stripe7.stripeRequest)(
|
|
4409
4734
|
{ secretKey: sk },
|
|
4410
4735
|
"GET",
|
|
4411
4736
|
`/v1/payment_intents/${encodeURIComponent(seat.providerPaymentIntentId)}`
|
|
4412
|
-
) : await (0,
|
|
4737
|
+
) : await (0, import_stripe7.stripeRequest)({ secretKey: sk }, "POST", "/v1/payment_intents", {
|
|
4413
4738
|
amount: seat.amountCents,
|
|
4414
4739
|
currency: seat.currency,
|
|
4415
4740
|
customer: seat.providerCustomerId,
|
|
@@ -4427,7 +4752,7 @@ async function attachNamedSeatStripeRenewal(sk, seat) {
|
|
|
4427
4752
|
const sub = await subscriptionForSeat(sk, seat);
|
|
4428
4753
|
const existing = sub.items.data.find((item2) => item2.metadata?.seatId === seat.id);
|
|
4429
4754
|
if (existing) return String(existing.id);
|
|
4430
|
-
const result = await (0,
|
|
4755
|
+
const result = await (0, import_stripe7.stripeRequest)({ secretKey: sk }, "POST", "/v1/subscription_items", {
|
|
4431
4756
|
subscription: seat.providerSubscriptionId,
|
|
4432
4757
|
price: seat.priceId,
|
|
4433
4758
|
quantity: 1,
|
|
@@ -4505,8 +4830,7 @@ async function confirmNamedMembershipSeatPayment(db, input, now) {
|
|
|
4505
4830
|
requireMembershipScope(member, input);
|
|
4506
4831
|
const row2 = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
|
|
4507
4832
|
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");
|
|
4833
|
+
const evidenceGuard = await commercialEvidenceGuard(db, input.eventId, row2.digest, "named_seat_payment_evidence_quarantined");
|
|
4510
4834
|
const fact = row2.fact;
|
|
4511
4835
|
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
4836
|
appId: input.appId,
|
|
@@ -4553,7 +4877,7 @@ async function confirmNamedMembershipSeatPayment(db, input, now) {
|
|
|
4553
4877
|
{ ns: "networkMembership", id: String(member.id), exists: true, equals: { revision: member.revision } },
|
|
4554
4878
|
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } },
|
|
4555
4879
|
{ ns: "networkCommercialEvents", id: String(row2.id), exists: true, equals: { digest: fact.digest } },
|
|
4556
|
-
|
|
4880
|
+
evidenceGuard
|
|
4557
4881
|
] });
|
|
4558
4882
|
} catch (error) {
|
|
4559
4883
|
if (error.code !== "transact_guard_failed") throw error;
|
|
@@ -4702,14 +5026,14 @@ async function recordRecipient(db, input, now, approve) {
|
|
|
4702
5026
|
}
|
|
4703
5027
|
|
|
4704
5028
|
// src/membership-seat-refund-stripe.ts
|
|
4705
|
-
var
|
|
5029
|
+
var import_stripe8 = require("@odla-ai/stripe");
|
|
4706
5030
|
var fail4 = (code = "named_seat_refund_mismatch") => {
|
|
4707
5031
|
throw Object.assign(new Error(code), { code });
|
|
4708
5032
|
};
|
|
4709
5033
|
async function refundNamedSeatStripePayment(secretKey, seat) {
|
|
4710
5034
|
if (!seat.providerPaymentIntentId || seat.status !== "refund_pending") fail4();
|
|
4711
5035
|
const call = async (method, path, params, idempotencyKey) => {
|
|
4712
|
-
const result = await (0,
|
|
5036
|
+
const result = await (0, import_stripe8.stripeRequest)({ secretKey }, method, path, params, idempotencyKey ? { idempotencyKey } : {});
|
|
4713
5037
|
if (!result.ok || !result.body) return fail4("named_seat_refund_provider_unavailable");
|
|
4714
5038
|
return result.body;
|
|
4715
5039
|
};
|
|
@@ -4831,7 +5155,7 @@ async function confirmNamedSeatProviderRefund(db, input, now) {
|
|
|
4831
5155
|
const row2 = await membershipLoad(db, "networkCommercialEvents", `network-commercial-event:${input.eventId}`);
|
|
4832
5156
|
if (!row2) return membershipFail("named_seat_refund_evidence_pending");
|
|
4833
5157
|
const fact = row2.fact;
|
|
4834
|
-
|
|
5158
|
+
await commercialEvidenceGuard(db, input.eventId, row2.digest, "named_seat_refund_evidence_quarantined");
|
|
4835
5159
|
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
5160
|
await requestNamedSeatRefund(db, { ...input, reason: "provider_refund" }, now);
|
|
4837
5161
|
}
|
|
@@ -5103,7 +5427,7 @@ function createMembershipAuthorityHandler(options) {
|
|
|
5103
5427
|
if (req.method !== "POST") return respond({ error: "method_not_allowed" }, 405);
|
|
5104
5428
|
const sender = req.headers.get(import_db.FEDERATION_HEADERS.sender);
|
|
5105
5429
|
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);
|
|
5430
|
+
if (!["offers", "checkouts", "payments", "approvals", "refunds", "renewals", "access", "restarts", "cancel-renewal", ...Object.keys(MEMBERSHIP_SEAT_ACTION_FIELDS)].includes(action ?? "")) return respond({ error: "unauthorized" }, 401);
|
|
5107
5431
|
const peer = peers.find((candidate) => candidate.appId === sender && url.pathname === `${MEMBERSHIP_AUTHORITY_PATH}/${candidate.runtime}/${action}` && !url.search);
|
|
5108
5432
|
if (!peer) return respond({ error: "unauthorized" }, 401);
|
|
5109
5433
|
let body;
|
|
@@ -5125,7 +5449,7 @@ function createMembershipAuthorityHandler(options) {
|
|
|
5125
5449
|
let input;
|
|
5126
5450
|
try {
|
|
5127
5451
|
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] ?? []];
|
|
5452
|
+
const fields = action === "seat-invitation" ? MEMBERSHIP_SEAT_ACTION_FIELDS[action] : ["applicationId", "skuId", ...action === "offers" ? ["requestId"] : action === "checkouts" ? ["offerId", "offerDigest", "quoteDigest", "priceId"] : action === "payments" ? ["eventId", "providerSubscriptionId", "providerCustomerId"] : action === "renewals" ? ["eventId"] : action === "restarts" ? ["previousSubscriptionId"] : MEMBERSHIP_SEAT_ACTION_FIELDS[action] ?? []];
|
|
5129
5453
|
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
5454
|
throw new TypeError("invalid request");
|
|
5131
5455
|
}
|
|
@@ -5150,6 +5474,15 @@ function createMembershipAuthorityHandler(options) {
|
|
|
5150
5474
|
applicationId: String(input.applicationId),
|
|
5151
5475
|
skuId: String(input.skuId)
|
|
5152
5476
|
};
|
|
5477
|
+
if (action === "restarts" || action === "cancel-renewal") {
|
|
5478
|
+
const stripeSecret = await getVaultSecret(db, options.stripeSecretName ?? "stripe_secret_key");
|
|
5479
|
+
if (!stripeSecret) return respond({ error: "membership_authority_unavailable" }, 503);
|
|
5480
|
+
if (action === "cancel-renewal") return respond({ ok: true, renewal: await cancelMembershipAtTermEnd(db, target, options.now?.() ?? Date.now(), stripeSecret) });
|
|
5481
|
+
return respond({ ok: true, restart: await prepareMembershipRestart(db, {
|
|
5482
|
+
...target,
|
|
5483
|
+
previousSubscriptionId: String(input.previousSubscriptionId)
|
|
5484
|
+
}, options.now?.() ?? Date.now(), stripeSecret) });
|
|
5485
|
+
}
|
|
5153
5486
|
if (action === "access") return respond({ ok: true, access: await readMembershipAccess(db, target, options.now?.() ?? Date.now()) });
|
|
5154
5487
|
if (action === "renewals") return respond({ ok: true, renewal: await reconcileMembershipBilling(
|
|
5155
5488
|
db,
|
|
@@ -5423,7 +5756,7 @@ async function requestMembershipRefund(options) {
|
|
|
5423
5756
|
async function requestMembershipRenewal(options) {
|
|
5424
5757
|
const result = await requestAuthority(options, "renewals", { applicationId: options.applicationId, skuId: options.skuId, eventId: options.eventId });
|
|
5425
5758
|
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)) {
|
|
5759
|
+
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
5760
|
throw new Error("invalid membership renewal receipt");
|
|
5428
5761
|
}
|
|
5429
5762
|
return { membershipId: row2.membershipId, status: row2.status, termEndsAt: row2.termEndsAt, graceEndsAt: row2.graceEndsAt, autoRenew: row2.autoRenew };
|
|
@@ -5465,9 +5798,25 @@ async function requestMembershipAccess(options) {
|
|
|
5465
5798
|
const result = await requestAuthority(options, "access", { applicationId: options.applicationId, skuId: options.skuId });
|
|
5466
5799
|
return verifyMembershipAccessReceipt(result.access, options, Date.now());
|
|
5467
5800
|
}
|
|
5801
|
+
async function requestMembershipRestart(options) {
|
|
5802
|
+
const result = await requestAuthority(options, "restarts", {
|
|
5803
|
+
applicationId: options.applicationId,
|
|
5804
|
+
skuId: options.skuId,
|
|
5805
|
+
previousSubscriptionId: options.previousSubscriptionId
|
|
5806
|
+
});
|
|
5807
|
+
const row2 = result.restart;
|
|
5808
|
+
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");
|
|
5809
|
+
return { applicationId: row2.applicationId, membershipId: row2.membershipId, restartId: row2.restartId, memberNumber: row2.memberNumber, status: "ready" };
|
|
5810
|
+
}
|
|
5811
|
+
async function requestMembershipCancellation(options) {
|
|
5812
|
+
const result = await requestAuthority(options, "cancel-renewal", { applicationId: options.applicationId, skuId: options.skuId });
|
|
5813
|
+
const row2 = result.renewal;
|
|
5814
|
+
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");
|
|
5815
|
+
return { membershipId: row2.membershipId, status: row2.status, termEndsAt: row2.termEndsAt, graceEndsAt: 0, autoRenew: false };
|
|
5816
|
+
}
|
|
5468
5817
|
|
|
5469
5818
|
// src/membership-seat-cancel.ts
|
|
5470
|
-
var
|
|
5819
|
+
var import_stripe9 = require("@odla-ai/stripe");
|
|
5471
5820
|
async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
|
|
5472
5821
|
membershipClock(now);
|
|
5473
5822
|
const state = await loadMembershipAuthorityState(db, target);
|
|
@@ -5495,7 +5844,7 @@ async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
|
|
|
5495
5844
|
}
|
|
5496
5845
|
const metadata = { ...stripePartitionMetadata(target), applicationId: target.applicationId, membershipId: seat.primaryMembershipId, seatId: seat.id };
|
|
5497
5846
|
const request = async (method, path, params) => {
|
|
5498
|
-
const result = await (0,
|
|
5847
|
+
const result = await (0, import_stripe9.stripeRequest)(
|
|
5499
5848
|
{ secretKey },
|
|
5500
5849
|
method,
|
|
5501
5850
|
path,
|
|
@@ -5508,7 +5857,7 @@ async function cancelUnpaidNamedSeat(db, target, secretKey, now) {
|
|
|
5508
5857
|
let payment;
|
|
5509
5858
|
if (seat.providerPaymentIntentId) payment = await request("GET", `/v1/payment_intents/${encodeURIComponent(String(seat.providerPaymentIntentId))}`);
|
|
5510
5859
|
else {
|
|
5511
|
-
const snapshot = await listStripeObjects((0,
|
|
5860
|
+
const snapshot = await listStripeObjects((0, import_stripe9.initStripe)({ secretKey }), "/v1/payment_intents", { customer: String(seat.providerCustomerId) });
|
|
5512
5861
|
const matches2 = snapshot.objects.filter((row2) => row2.metadata?.seatId === target.seatId);
|
|
5513
5862
|
if (!snapshot.complete || matches2.length !== 1) throw new Error("seat payment creation outcome unresolved");
|
|
5514
5863
|
payment = matches2[0];
|
|
@@ -5732,7 +6081,27 @@ var membershipAuthorityEntities = {
|
|
|
5732
6081
|
cancellationRequestedAt: attr9("number", true),
|
|
5733
6082
|
graceEndsAt: attr9("number", true),
|
|
5734
6083
|
...times(),
|
|
5735
|
-
refundRequestedAt: attr9("number", true)
|
|
6084
|
+
refundRequestedAt: attr9("number", true),
|
|
6085
|
+
restartId: key2(true)
|
|
6086
|
+
} },
|
|
6087
|
+
membershipRestart: { attrs: {
|
|
6088
|
+
id: id9(),
|
|
6089
|
+
...scope(),
|
|
6090
|
+
membershipId: key2(),
|
|
6091
|
+
restartId: key2(),
|
|
6092
|
+
previousSubscriptionId: key2(),
|
|
6093
|
+
memberNumber: attr9("number"),
|
|
6094
|
+
status: key2(),
|
|
6095
|
+
createdAt: attr9("number")
|
|
6096
|
+
} },
|
|
6097
|
+
membershipPurchaseHistory: { attrs: {
|
|
6098
|
+
id: id9(),
|
|
6099
|
+
...scope(),
|
|
6100
|
+
membershipId: key2(),
|
|
6101
|
+
providerSubscriptionId: key2(),
|
|
6102
|
+
memberSnapshot: attr9("json"),
|
|
6103
|
+
bindingSnapshot: attr9("json"),
|
|
6104
|
+
archivedAt: attr9("number")
|
|
5736
6105
|
} },
|
|
5737
6106
|
networkEntitlement: { attrs: {
|
|
5738
6107
|
id: id9(),
|
|
@@ -6531,7 +6900,7 @@ async function reconcileLifecycleDelivery(db, input, now) {
|
|
|
6531
6900
|
}
|
|
6532
6901
|
|
|
6533
6902
|
// src/payments.ts
|
|
6534
|
-
var
|
|
6903
|
+
var import_stripe10 = require("@odla-ai/stripe");
|
|
6535
6904
|
function paymentsReady(group, hasSecretKey) {
|
|
6536
6905
|
return Boolean(group.stripePublishableKey && group.stripePriceId && hasSecretKey);
|
|
6537
6906
|
}
|
|
@@ -7158,9 +7527,9 @@ function sharedRecordFromCrm(crm, record4, target, sourceId) {
|
|
|
7158
7527
|
}
|
|
7159
7528
|
const nameField = def.nameField ?? "name";
|
|
7160
7529
|
const input = {};
|
|
7161
|
-
for (const
|
|
7162
|
-
const value = record4.fields?.[
|
|
7163
|
-
if (value !== void 0) input[
|
|
7530
|
+
for (const field2 of /* @__PURE__ */ new Set([nameField, ...fields])) {
|
|
7531
|
+
const value = record4.fields?.[field2];
|
|
7532
|
+
if (value !== void 0) input[field2] = value;
|
|
7164
7533
|
}
|
|
7165
7534
|
if (input[nameField] === void 0) input[nameField] = record4.name;
|
|
7166
7535
|
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 +7798,9 @@ function subAnnualCents(sub) {
|
|
|
7429
7798
|
}
|
|
7430
7799
|
|
|
7431
7800
|
// src/payments-stripe.ts
|
|
7432
|
-
var
|
|
7801
|
+
var import_stripe11 = require("@odla-ai/stripe");
|
|
7433
7802
|
async function stripeCall(sk, method, path, params, idempotencyKey) {
|
|
7434
|
-
return (0,
|
|
7803
|
+
return (0, import_stripe11.stripeRequest)(
|
|
7435
7804
|
{ secretKey: sk },
|
|
7436
7805
|
method,
|
|
7437
7806
|
path,
|
|
@@ -7741,7 +8110,8 @@ function applicationSummary(app) {
|
|
|
7741
8110
|
status: app.status,
|
|
7742
8111
|
createdAt: app.createdAt ?? null,
|
|
7743
8112
|
meetingLink: app.meetingLink ?? null,
|
|
7744
|
-
paid: Boolean(app.stripeSubscriptionId) && app.status !== "refunded",
|
|
8113
|
+
paid: Boolean(app.stripeSubscriptionId) && app.status !== "refunded" && app.canceled !== true && !["lapsed", "refunded", "refund_pending", "canceled", "restarting"].includes(app.membershipStatus ?? ""),
|
|
8114
|
+
...app.membershipStatus ? { membershipStatus: app.membershipStatus, membershipGraceEndsAt: app.membershipGraceEndsAt, membershipAutoRenew: app.membershipAutoRenew } : {},
|
|
7745
8115
|
renewalAt: app.renewalAt ?? null,
|
|
7746
8116
|
canceled: app.canceled === true,
|
|
7747
8117
|
tierId: app.tierId ?? null
|
|
@@ -7923,7 +8293,7 @@ function isValidTimeZone(tz) {
|
|
|
7923
8293
|
function resolveScheduling(config) {
|
|
7924
8294
|
const result = validateScheduling(config);
|
|
7925
8295
|
if (result.ok) return result.value;
|
|
7926
|
-
const detail = Object.entries(result.errors).map(([
|
|
8296
|
+
const detail = Object.entries(result.errors).map(([field2, message]) => `${field2}: ${message}`).join(" ");
|
|
7927
8297
|
throw new Error(`scheduling: ${detail}`);
|
|
7928
8298
|
}
|
|
7929
8299
|
function validateScheduling(config) {
|
|
@@ -9068,7 +9438,7 @@ var MEMBER_SELF_READABLE_FIELDS = new Set(
|
|
|
9068
9438
|
MEMBER_SYSTEM_AUTHORITY_MATRIX.filter((row2) => row2.memberAccess !== "none").map((row2) => row2.id)
|
|
9069
9439
|
);
|
|
9070
9440
|
function projectMemberSelf(record4) {
|
|
9071
|
-
return Object.fromEntries(Object.entries(record4).filter(([
|
|
9441
|
+
return Object.fromEntries(Object.entries(record4).filter(([field2]) => MEMBER_SELF_READABLE_FIELDS.has(field2)));
|
|
9072
9442
|
}
|
|
9073
9443
|
function classifyMemberSystemWrite(intent) {
|
|
9074
9444
|
if (intent.actor === "bnf") return "reject_bnf_surface";
|