@odla-ai/chapter 0.53.0 → 0.54.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 +28 -1
- package/dist/{chunk-PTC43J7F.js → chunk-NDZCR3OW.js} +5 -5
- package/dist/chunk-NDZCR3OW.js.map +1 -0
- package/dist/{chunk-TEHCORNC.js → chunk-SPEL4OGE.js} +57 -23
- package/dist/chunk-SPEL4OGE.js.map +1 -0
- package/dist/index.cjs +219 -106
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +30 -10
- package/dist/index.d.ts +30 -10
- package/dist/index.js +219 -106
- package/dist/index.js.map +1 -1
- package/dist/ui/admin/index.js +1 -1
- package/dist/ui/index.d.ts +1 -1
- package/dist/ui/index.js +8 -2
- package/dist/ui/member/index.d.ts +32 -1
- package/dist/ui/member/index.js +7 -1
- package/dist/worker/index.cjs +296 -128
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +84 -9
- package/dist/worker/index.d.ts +84 -9
- package/dist/worker/index.js +296 -128
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-PTC43J7F.js.map +0 -1
- package/dist/chunk-TEHCORNC.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -3286,8 +3286,8 @@ var object = (value) => {
|
|
|
3286
3286
|
var id6 = (value) => typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9_.:-]{2,159}$/.test(value) ? value : fail2("id");
|
|
3287
3287
|
var integer2 = (value, min, max) => typeof value === "number" && Number.isSafeInteger(value) && value >= min && value <= max ? value : fail2("integer");
|
|
3288
3288
|
var time = (value) => {
|
|
3289
|
-
const
|
|
3290
|
-
return integer2(
|
|
3289
|
+
const at2 = typeof value === "number" ? value : typeof value === "string" ? Date.parse(value) : NaN;
|
|
3290
|
+
return integer2(at2, 0, 864e13);
|
|
3291
3291
|
};
|
|
3292
3292
|
var ids = (value, allowEmpty = false) => {
|
|
3293
3293
|
if (!Array.isArray(value) || value.length > 100 || !allowEmpty && value.length === 0) return fail2("ids");
|
|
@@ -3331,7 +3331,7 @@ function membershipOfferingVersion(value) {
|
|
|
3331
3331
|
term: normalizedTerm,
|
|
3332
3332
|
autoRenew: true,
|
|
3333
3333
|
paymentGraceDays: integer2(input.paymentGraceDays, 0, 60),
|
|
3334
|
-
seatPriceCents: integer2(input.seatPriceCents,
|
|
3334
|
+
seatPriceCents: integer2(input.seatPriceCents, 0, Number.MAX_SAFE_INTEGER),
|
|
3335
3335
|
...input.seatPurchaseWindowCalendarMonths === void 0 || input.seatPurchaseWindowCalendarMonths === null ? {} : { seatPurchaseWindowCalendarMonths: integer2(input.seatPurchaseWindowCalendarMonths, 1, 120) },
|
|
3336
3336
|
requiredDisclosures: ids(input.requiredDisclosures)
|
|
3337
3337
|
};
|
|
@@ -4103,6 +4103,181 @@ async function refundNetworkMembership(db, input, secretKey, now) {
|
|
|
4103
4103
|
// src/membership-provider-lifecycle.ts
|
|
4104
4104
|
import { stripeRequest as stripeRequest4 } from "@odla-ai/stripe";
|
|
4105
4105
|
|
|
4106
|
+
// src/membership-seat-reservation.ts
|
|
4107
|
+
async function recipientKey(input, email) {
|
|
4108
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify([input.appId, input.chapterId, input.environment, input.runtime, email])));
|
|
4109
|
+
return `seat-recipient:${[...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
4110
|
+
}
|
|
4111
|
+
async function reserveNamedMembershipSeat(db, input, now) {
|
|
4112
|
+
membershipClock(now);
|
|
4113
|
+
const name = typeof input.recipientName === "string" ? input.recipientName.trim().replace(/\s+/g, " ") : "";
|
|
4114
|
+
const email = normalizeChapterEmail(typeof input.recipientEmail === "string" ? input.recipientEmail : "");
|
|
4115
|
+
const purchaser = normalizeChapterEmail(typeof input.purchaserEmail === "string" ? input.purchaserEmail : "");
|
|
4116
|
+
if (name.length < 2 || name.length > 160 || /[\u0000-\u001f\u007f]/.test(input.recipientName) || email.length > 254 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) || !purchaser || email === purchaser || !/^price_[A-Za-z0-9_]+$/.test(input.priceId) || !/^user_[A-Za-z0-9_]+$/.test(input.purchaserUserId)) membershipFail("invalid_named_seat_recipient");
|
|
4117
|
+
const state = await loadMembershipAuthorityState(db, input);
|
|
4118
|
+
const memberId = await networkMembershipId(input), member = await membershipLoad(db, "networkMembership", memberId);
|
|
4119
|
+
requireMembershipScope(member, input);
|
|
4120
|
+
if (!Number.isSafeInteger(member.revision) || !/^price_[A-Za-z0-9_]+$/.test(String(member.providerPriceId)) || member.providerPriceId === input.priceId) membershipFail("named_seat_purchase_unavailable");
|
|
4121
|
+
const id10 = (await membershipOfferIdForRequest({ ...input, requestId: "named_seat" })).replace(/^offer_/, "seat_");
|
|
4122
|
+
let seat = await membershipLoad(db, "networkSeat", id10);
|
|
4123
|
+
if (!seat) {
|
|
4124
|
+
const offering = membershipOfferingVersion(member.offeringSnapshot), claimId = await recipientKey(input, email);
|
|
4125
|
+
const purchaseEndsAt = seatPurchaseDeadline(offering, member);
|
|
4126
|
+
if (!["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) || Number(member.refundedCents) !== 0 || now < Number(member.initialTermStartedAt) || now >= Number(member.termEndsAt) || now > purchaseEndsAt || !Number.isSafeInteger(purchaseEndsAt) || member.namedSeatId !== void 0) {
|
|
4127
|
+
membershipFail("named_seat_purchase_unavailable");
|
|
4128
|
+
}
|
|
4129
|
+
const attrs = {
|
|
4130
|
+
id: id10,
|
|
4131
|
+
...membershipScope(input),
|
|
4132
|
+
primaryMembershipId: memberId,
|
|
4133
|
+
recipientName: name,
|
|
4134
|
+
recipientEmailNormalized: email,
|
|
4135
|
+
purchaserUserId: input.purchaserUserId,
|
|
4136
|
+
recipientClaimId: claimId,
|
|
4137
|
+
priceId: input.priceId,
|
|
4138
|
+
primaryPriceId: member.providerPriceId,
|
|
4139
|
+
amountCents: offering.seatPriceCents,
|
|
4140
|
+
currency: offering.currency,
|
|
4141
|
+
interval: offering.interval,
|
|
4142
|
+
providerCustomerId: member.providerCustomerId,
|
|
4143
|
+
providerSubscriptionId: member.providerSubscriptionId,
|
|
4144
|
+
status: "payment_pending",
|
|
4145
|
+
preparedAt: now,
|
|
4146
|
+
termEndsAt: member.termEndsAt,
|
|
4147
|
+
seatPurchaseEndsAt: purchaseEndsAt,
|
|
4148
|
+
offeringSnapshot: offering,
|
|
4149
|
+
revision: 1,
|
|
4150
|
+
createdAt: now,
|
|
4151
|
+
updatedAt: now
|
|
4152
|
+
};
|
|
4153
|
+
try {
|
|
4154
|
+
await db.transact([
|
|
4155
|
+
membershipUpdate("networkSeat", id10, attrs),
|
|
4156
|
+
membershipUpdate("networkSeatRecipient", claimId, {
|
|
4157
|
+
id: claimId,
|
|
4158
|
+
appId: input.appId,
|
|
4159
|
+
chapterId: input.chapterId,
|
|
4160
|
+
environment: input.environment,
|
|
4161
|
+
runtime: input.runtime,
|
|
4162
|
+
seatId: id10,
|
|
4163
|
+
recipientEmailNormalized: email,
|
|
4164
|
+
createdAt: now
|
|
4165
|
+
}),
|
|
4166
|
+
membershipUpdate("networkMembership", memberId, { namedSeatId: id10, revision: Number(member.revision) + 1, updatedAt: now })
|
|
4167
|
+
], { mutationId: `named-seat-reserve:${id10}`, guards: [
|
|
4168
|
+
{ ns: "networkSeat", id: id10, exists: false },
|
|
4169
|
+
{ ns: "networkSeatRecipient", id: claimId, exists: false },
|
|
4170
|
+
{ ns: "networkMembership", id: memberId, exists: true, equals: { revision: member.revision }, absent: ["namedSeatId"] },
|
|
4171
|
+
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
|
|
4172
|
+
] });
|
|
4173
|
+
} catch (error) {
|
|
4174
|
+
if (error.code !== "transact_guard_failed") throw error;
|
|
4175
|
+
}
|
|
4176
|
+
seat = await membershipLoad(db, "networkSeat", id10);
|
|
4177
|
+
if (!seat && await membershipLoad(db, "networkSeatRecipient", claimId)) membershipFail("named_seat_recipient_unavailable");
|
|
4178
|
+
}
|
|
4179
|
+
requireMembershipScope(seat, input);
|
|
4180
|
+
if (seat.primaryMembershipId !== memberId || seat.recipientName !== name || seat.recipientEmailNormalized !== email || seat.priceId !== input.priceId || seat.purchaserUserId !== input.purchaserUserId) membershipFail("named_seat_purchase_conflict");
|
|
4181
|
+
return seat;
|
|
4182
|
+
}
|
|
4183
|
+
|
|
4184
|
+
// src/membership-seat-included.ts
|
|
4185
|
+
var namedSeatIncluded = (seat) => Number(seat.amountCents) === 0;
|
|
4186
|
+
var namedSeatSettled = (seat) => namedSeatIncluded(seat) ? seat.purchasedAt !== void 0 : Boolean(seat.providerPaymentIntentId && seat.providerSubscriptionItemId);
|
|
4187
|
+
function namedSeatRecipientState(seat) {
|
|
4188
|
+
switch (String(seat.status)) {
|
|
4189
|
+
case "renewal_pending":
|
|
4190
|
+
case "pending_acceptance":
|
|
4191
|
+
return "invited";
|
|
4192
|
+
case "pending_approval":
|
|
4193
|
+
case "pending_primary":
|
|
4194
|
+
return "accepted";
|
|
4195
|
+
case "active":
|
|
4196
|
+
return "joined";
|
|
4197
|
+
case "refund_pending":
|
|
4198
|
+
case "refunded":
|
|
4199
|
+
return seat.refundReason === "declined" ? "declined" : "refunded";
|
|
4200
|
+
case "canceled":
|
|
4201
|
+
return "canceled";
|
|
4202
|
+
default:
|
|
4203
|
+
return "payment_pending";
|
|
4204
|
+
}
|
|
4205
|
+
}
|
|
4206
|
+
var OPEN = ["pending_acceptance", "pending_approval", "pending_primary", "active"];
|
|
4207
|
+
async function grantIncludedNamedSeat(db, input, now) {
|
|
4208
|
+
membershipClock(now);
|
|
4209
|
+
const seat = await reserveNamedMembershipSeat(db, input, now);
|
|
4210
|
+
if (!namedSeatIncluded(seat)) membershipFail("named_seat_not_included");
|
|
4211
|
+
const state = await loadMembershipAuthorityState(db, input);
|
|
4212
|
+
const member = await membershipLoad(db, "networkMembership", seat.primaryMembershipId);
|
|
4213
|
+
requireMembershipScope(member, input);
|
|
4214
|
+
if (!["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) || Number(member.refundedCents) !== 0 || member.namedSeatId !== seat.id || now >= Number(member.termEndsAt)) {
|
|
4215
|
+
membershipFail("named_seat_purchase_unavailable");
|
|
4216
|
+
}
|
|
4217
|
+
if (seat.status === "payment_pending") {
|
|
4218
|
+
const entitlementId = `${seat.id}:entitlement`, inviteId = `${seat.id}:invite`, source = `included:${seat.id}`;
|
|
4219
|
+
try {
|
|
4220
|
+
await db.transact([
|
|
4221
|
+
membershipUpdate("networkSeat", seat.id, {
|
|
4222
|
+
status: "pending_acceptance",
|
|
4223
|
+
purchasedAt: now,
|
|
4224
|
+
termStartedAt: now,
|
|
4225
|
+
termEndsAt: member.termEndsAt,
|
|
4226
|
+
acceptanceEndsAt: Number(member.termEndsAt),
|
|
4227
|
+
entitlementId,
|
|
4228
|
+
revision: Number(seat.revision) + 1,
|
|
4229
|
+
updatedAt: now
|
|
4230
|
+
}),
|
|
4231
|
+
membershipUpdate("networkEntitlement", entitlementId, {
|
|
4232
|
+
id: entitlementId,
|
|
4233
|
+
...membershipScope(input),
|
|
4234
|
+
membershipId: member.id,
|
|
4235
|
+
seatId: seat.id,
|
|
4236
|
+
kind: "named_seat",
|
|
4237
|
+
status: "pending_acceptance",
|
|
4238
|
+
termStartedAt: now,
|
|
4239
|
+
termEndsAt: member.termEndsAt,
|
|
4240
|
+
offeringSnapshot: member.offeringSnapshot,
|
|
4241
|
+
sourceEventId: source,
|
|
4242
|
+
revision: 1,
|
|
4243
|
+
createdAt: now,
|
|
4244
|
+
updatedAt: now
|
|
4245
|
+
}),
|
|
4246
|
+
membershipUpdate("membershipEffect", inviteId, {
|
|
4247
|
+
id: inviteId,
|
|
4248
|
+
chapterId: input.chapterId,
|
|
4249
|
+
environment: input.environment,
|
|
4250
|
+
runtime: input.runtime,
|
|
4251
|
+
membershipId: member.id,
|
|
4252
|
+
seatId: seat.id,
|
|
4253
|
+
kind: "invite_named_recipient",
|
|
4254
|
+
status: "pending",
|
|
4255
|
+
attempts: 0,
|
|
4256
|
+
createdAt: now,
|
|
4257
|
+
updatedAt: now
|
|
4258
|
+
})
|
|
4259
|
+
], { mutationId: `named-seat-included:${seat.id}`, guards: [
|
|
4260
|
+
{
|
|
4261
|
+
ns: "networkSeat",
|
|
4262
|
+
id: seat.id,
|
|
4263
|
+
exists: true,
|
|
4264
|
+
equals: { status: "payment_pending", revision: seat.revision, amountCents: 0 },
|
|
4265
|
+
absent: ["purchasedAt", "providerPaymentIntentId"]
|
|
4266
|
+
},
|
|
4267
|
+
{ ns: "networkEntitlement", id: entitlementId, exists: false },
|
|
4268
|
+
{ ns: "networkMembership", id: String(member.id), exists: true, equals: { revision: member.revision, namedSeatId: seat.id } },
|
|
4269
|
+
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
|
|
4270
|
+
] });
|
|
4271
|
+
} catch (error) {
|
|
4272
|
+
if (error.code !== "transact_guard_failed") throw error;
|
|
4273
|
+
}
|
|
4274
|
+
}
|
|
4275
|
+
const current = await membershipLoad(db, "networkSeat", seat.id);
|
|
4276
|
+
requireMembershipScope(current, input);
|
|
4277
|
+
if (!namedSeatIncluded(current) || current.purchasedAt === void 0 || !OPEN.includes(String(current.status))) membershipFail("named_seat_purchase_unavailable");
|
|
4278
|
+
return { seatId: seat.id, amountCents: 0, currency: seat.currency, termEndsAt: Number(current.termEndsAt), status: String(current.status), included: true };
|
|
4279
|
+
}
|
|
4280
|
+
|
|
4106
4281
|
// src/membership-renewal-authority.ts
|
|
4107
4282
|
async function reconcileMembershipRenewal(db, input, now) {
|
|
4108
4283
|
membershipClock(now);
|
|
@@ -4138,7 +4313,9 @@ async function reconcileMembershipRenewal(db, input, now) {
|
|
|
4138
4313
|
if (start !== member.termEndsAt || start > now + 3e4) membershipFail("membership_renewal_period_gap");
|
|
4139
4314
|
const seat = member.namedSeatId ? await membershipLoad(db, "networkSeat", String(member.namedSeatId)) : void 0;
|
|
4140
4315
|
if (seat) requireMembershipScope(seat, input);
|
|
4141
|
-
const
|
|
4316
|
+
const openSeat = seat && !["refunded", "refund_pending", "canceled"].includes(String(seat.status)) ? seat : void 0;
|
|
4317
|
+
const billedSeat = openSeat && openSeat.providerSubscriptionId === member.providerSubscriptionId && openSeat.providerSubscriptionItemId ? openSeat : void 0;
|
|
4318
|
+
const carriedSeat = billedSeat ?? (openSeat && namedSeatIncluded(openSeat) && openSeat.purchasedAt !== void 0 ? openSeat : void 0);
|
|
4142
4319
|
const prices = [String(member.providerPriceId), ...billedSeat ? [String(billedSeat.priceId)] : []].sort();
|
|
4143
4320
|
const amount = Number(member.renewalAmountCents) + Number(billedSeat?.amountCents ?? 0);
|
|
4144
4321
|
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");
|
|
@@ -4178,19 +4355,19 @@ async function reconcileMembershipRenewal(db, input, now) {
|
|
|
4178
4355
|
evidenceGuard
|
|
4179
4356
|
];
|
|
4180
4357
|
if (seat) guards.push({ ns: "networkSeat", id: String(seat.id), exists: true, equals: { revision: seat.revision } });
|
|
4181
|
-
if (
|
|
4182
|
-
const seatEntitlement = await membershipLoad(db, "networkEntitlement", String(
|
|
4358
|
+
if (carriedSeat?.entitlementId) {
|
|
4359
|
+
const seatEntitlement = await membershipLoad(db, "networkEntitlement", String(carriedSeat.entitlementId));
|
|
4183
4360
|
requireMembershipScope(seatEntitlement, input);
|
|
4184
4361
|
guards.push({ ns: "networkEntitlement", id: String(seatEntitlement.id), exists: true, equals: { revision: seatEntitlement.revision } });
|
|
4185
4362
|
ops.push(
|
|
4186
|
-
membershipUpdate("networkSeat", String(
|
|
4363
|
+
membershipUpdate("networkSeat", String(carriedSeat.id), {
|
|
4187
4364
|
...paid ? { termStartedAt: start, termEndsAt: end } : {},
|
|
4188
|
-
revision: Number(
|
|
4365
|
+
revision: Number(carriedSeat.revision) + 1,
|
|
4189
4366
|
updatedAt: now
|
|
4190
4367
|
}),
|
|
4191
4368
|
membershipUpdate("networkEntitlement", String(seatEntitlement.id), {
|
|
4192
4369
|
...paid ? { termStartedAt: start, termEndsAt: end } : {},
|
|
4193
|
-
status:
|
|
4370
|
+
status: carriedSeat.journeyApprovedAt ? entitlementStatus : seatEntitlement.status,
|
|
4194
4371
|
revision: Number(seatEntitlement.revision) + 1,
|
|
4195
4372
|
updatedAt: now
|
|
4196
4373
|
})
|
|
@@ -4442,84 +4619,6 @@ async function verifyMembershipAccessReceipt(value, input, now) {
|
|
|
4442
4619
|
return { membershipId: row2.membershipId, allowed: row2.allowed, status: row2.status, accessEndsAt: row2.accessEndsAt, revision: row2.revision, ...row2.restartEligible === true && !row2.allowed ? { restartEligible: true } : {} };
|
|
4443
4620
|
}
|
|
4444
4621
|
|
|
4445
|
-
// src/membership-seat-reservation.ts
|
|
4446
|
-
async function recipientKey(input, email) {
|
|
4447
|
-
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify([input.appId, input.chapterId, input.environment, input.runtime, email])));
|
|
4448
|
-
return `seat-recipient:${[...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
4449
|
-
}
|
|
4450
|
-
async function reserveNamedMembershipSeat(db, input, now) {
|
|
4451
|
-
membershipClock(now);
|
|
4452
|
-
const name = typeof input.recipientName === "string" ? input.recipientName.trim().replace(/\s+/g, " ") : "";
|
|
4453
|
-
const email = normalizeChapterEmail(typeof input.recipientEmail === "string" ? input.recipientEmail : "");
|
|
4454
|
-
const purchaser = normalizeChapterEmail(typeof input.purchaserEmail === "string" ? input.purchaserEmail : "");
|
|
4455
|
-
if (name.length < 2 || name.length > 160 || /[\u0000-\u001f\u007f]/.test(input.recipientName) || email.length > 254 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) || !purchaser || email === purchaser || !/^price_[A-Za-z0-9_]+$/.test(input.priceId) || !/^user_[A-Za-z0-9_]+$/.test(input.purchaserUserId)) membershipFail("invalid_named_seat_recipient");
|
|
4456
|
-
const state = await loadMembershipAuthorityState(db, input);
|
|
4457
|
-
const memberId = await networkMembershipId(input), member = await membershipLoad(db, "networkMembership", memberId);
|
|
4458
|
-
requireMembershipScope(member, input);
|
|
4459
|
-
if (!Number.isSafeInteger(member.revision) || !/^price_[A-Za-z0-9_]+$/.test(String(member.providerPriceId)) || member.providerPriceId === input.priceId) membershipFail("named_seat_purchase_unavailable");
|
|
4460
|
-
const id10 = (await membershipOfferIdForRequest({ ...input, requestId: "named_seat" })).replace(/^offer_/, "seat_");
|
|
4461
|
-
let seat = await membershipLoad(db, "networkSeat", id10);
|
|
4462
|
-
if (!seat) {
|
|
4463
|
-
const offering = membershipOfferingVersion(member.offeringSnapshot), claimId = await recipientKey(input, email);
|
|
4464
|
-
const purchaseEndsAt = seatPurchaseDeadline(offering, member);
|
|
4465
|
-
if (!["paid_pending_approval", "active", "cancel_at_term_end"].includes(String(member.status)) || Number(member.refundedCents) !== 0 || now < Number(member.initialTermStartedAt) || now >= Number(member.termEndsAt) || now > purchaseEndsAt || !Number.isSafeInteger(purchaseEndsAt) || member.namedSeatId !== void 0) {
|
|
4466
|
-
membershipFail("named_seat_purchase_unavailable");
|
|
4467
|
-
}
|
|
4468
|
-
const attrs = {
|
|
4469
|
-
id: id10,
|
|
4470
|
-
...membershipScope(input),
|
|
4471
|
-
primaryMembershipId: memberId,
|
|
4472
|
-
recipientName: name,
|
|
4473
|
-
recipientEmailNormalized: email,
|
|
4474
|
-
purchaserUserId: input.purchaserUserId,
|
|
4475
|
-
recipientClaimId: claimId,
|
|
4476
|
-
priceId: input.priceId,
|
|
4477
|
-
primaryPriceId: member.providerPriceId,
|
|
4478
|
-
amountCents: offering.seatPriceCents,
|
|
4479
|
-
currency: offering.currency,
|
|
4480
|
-
interval: offering.interval,
|
|
4481
|
-
providerCustomerId: member.providerCustomerId,
|
|
4482
|
-
providerSubscriptionId: member.providerSubscriptionId,
|
|
4483
|
-
status: "payment_pending",
|
|
4484
|
-
preparedAt: now,
|
|
4485
|
-
termEndsAt: member.termEndsAt,
|
|
4486
|
-
seatPurchaseEndsAt: purchaseEndsAt,
|
|
4487
|
-
offeringSnapshot: offering,
|
|
4488
|
-
revision: 1,
|
|
4489
|
-
createdAt: now,
|
|
4490
|
-
updatedAt: now
|
|
4491
|
-
};
|
|
4492
|
-
try {
|
|
4493
|
-
await db.transact([
|
|
4494
|
-
membershipUpdate("networkSeat", id10, attrs),
|
|
4495
|
-
membershipUpdate("networkSeatRecipient", claimId, {
|
|
4496
|
-
id: claimId,
|
|
4497
|
-
appId: input.appId,
|
|
4498
|
-
chapterId: input.chapterId,
|
|
4499
|
-
environment: input.environment,
|
|
4500
|
-
runtime: input.runtime,
|
|
4501
|
-
seatId: id10,
|
|
4502
|
-
recipientEmailNormalized: email,
|
|
4503
|
-
createdAt: now
|
|
4504
|
-
}),
|
|
4505
|
-
membershipUpdate("networkMembership", memberId, { namedSeatId: id10, revision: Number(member.revision) + 1, updatedAt: now })
|
|
4506
|
-
], { mutationId: `named-seat-reserve:${id10}`, guards: [
|
|
4507
|
-
{ ns: "networkSeat", id: id10, exists: false },
|
|
4508
|
-
{ ns: "networkSeatRecipient", id: claimId, exists: false },
|
|
4509
|
-
{ ns: "networkMembership", id: memberId, exists: true, equals: { revision: member.revision }, absent: ["namedSeatId"] },
|
|
4510
|
-
{ ns: "membershipAuthorityState", id: String(state.id), exists: true, equals: { mode: "authoritative", revision: state.revision } }
|
|
4511
|
-
] });
|
|
4512
|
-
} catch (error) {
|
|
4513
|
-
if (error.code !== "transact_guard_failed") throw error;
|
|
4514
|
-
}
|
|
4515
|
-
seat = await membershipLoad(db, "networkSeat", id10);
|
|
4516
|
-
if (!seat && await membershipLoad(db, "networkSeatRecipient", claimId)) membershipFail("named_seat_recipient_unavailable");
|
|
4517
|
-
}
|
|
4518
|
-
requireMembershipScope(seat, input);
|
|
4519
|
-
if (seat.primaryMembershipId !== memberId || seat.recipientName !== name || seat.recipientEmailNormalized !== email || seat.priceId !== input.priceId || seat.purchaserUserId !== input.purchaserUserId) membershipFail("named_seat_purchase_conflict");
|
|
4520
|
-
return seat;
|
|
4521
|
-
}
|
|
4522
|
-
|
|
4523
4622
|
// src/membership-seat-stripe.ts
|
|
4524
4623
|
import { stripeRequest as stripeRequest7 } from "@odla-ai/stripe";
|
|
4525
4624
|
|
|
@@ -4622,6 +4721,7 @@ async function attachNamedSeatStripeRenewal(sk, seat) {
|
|
|
4622
4721
|
// src/membership-seat-checkout.ts
|
|
4623
4722
|
async function prepareNamedMembershipSeatPayment(db, input, secretKey, now) {
|
|
4624
4723
|
let seat = await reserveNamedMembershipSeat(db, input, now);
|
|
4724
|
+
if (namedSeatIncluded(seat)) membershipFail("named_seat_included");
|
|
4625
4725
|
const state = await loadMembershipAuthorityState(db, input);
|
|
4626
4726
|
const member = await membershipLoad(db, "networkMembership", seat.primaryMembershipId);
|
|
4627
4727
|
requireMembershipScope(member, input);
|
|
@@ -4828,7 +4928,7 @@ async function recordRecipient(db, input, now, approve) {
|
|
|
4828
4928
|
requireMembershipScope(member, input);
|
|
4829
4929
|
const entitlement = await membershipLoad(db, "networkEntitlement", String(seat.entitlementId));
|
|
4830
4930
|
requireMembershipScope(entitlement, input);
|
|
4831
|
-
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
|
|
4931
|
+
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) || !namedSeatSettled(seat)) membershipFail("named_seat_acceptance_unavailable");
|
|
4832
4932
|
if (seat.acceptedAt === void 0) {
|
|
4833
4933
|
if (approve || normalizeChapterEmail(input.verifiedRecipientEmail) !== seat.recipientEmailNormalized) {
|
|
4834
4934
|
membershipFail("named_seat_recipient_verification_required");
|
|
@@ -4943,7 +5043,7 @@ async function requestNamedSeatRefund(db, input, now) {
|
|
|
4943
5043
|
const member = await membershipLoad(db, "networkMembership", String(seat.primaryMembershipId));
|
|
4944
5044
|
requireMembershipScope(member, input);
|
|
4945
5045
|
if (seat.status === "refunded" || seat.status === "refund_pending") return;
|
|
4946
|
-
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");
|
|
5046
|
+
if (!seat.purchasedAt || !seat.providerPaymentIntentId && !namedSeatIncluded(seat) || input.reason === "declined" && seat.journeyApprovedAt !== void 0 || input.reason === "primary_closed" && !["refunded", "declined"].includes(String(member.status))) membershipFail("named_seat_refund_not_eligible");
|
|
4947
5047
|
const entitlement = seat.entitlementId ? await membershipLoad(db, "networkEntitlement", String(seat.entitlementId)) : void 0;
|
|
4948
5048
|
await db.transact([
|
|
4949
5049
|
membershipUpdate("networkSeat", input.seatId, { status: "refund_pending", refundReason: input.reason, revision: Number(seat.revision) + 1, updatedAt: now }),
|
|
@@ -4988,7 +5088,7 @@ async function completeNamedSeatRefund(db, seatId, secretKey, now) {
|
|
|
4988
5088
|
await loadMembershipAuthorityState(db, seat);
|
|
4989
5089
|
if (seat.status === "refunded") return;
|
|
4990
5090
|
if (seat.status !== "refund_pending") membershipFail("named_seat_refund_not_ready");
|
|
4991
|
-
const result = await refundNamedSeatStripePayment(secretKey, seat);
|
|
5091
|
+
const result = namedSeatIncluded(seat) ? { refundedCents: 0, refundIds: [] } : await refundNamedSeatStripePayment(secretKey, seat);
|
|
4992
5092
|
await db.transact([
|
|
4993
5093
|
membershipUpdate("networkSeat", seatId, {
|
|
4994
5094
|
status: "refunded",
|
|
@@ -5022,7 +5122,7 @@ async function refundNamedSeatByOperator(db, input, stripeSecretKey, now) {
|
|
|
5022
5122
|
await requestNamedSeatRefund(db, { ...input, reason: "operator" }, now);
|
|
5023
5123
|
await completeNamedSeatRefund(db, input.seatId, stripeSecretKey, now);
|
|
5024
5124
|
const refunded = await membershipLoad(db, "networkSeat", input.seatId);
|
|
5025
|
-
if (refunded?.status !== "refunded" || !Number.isSafeInteger(refunded.refundedCents) || Number(refunded.refundedCents)
|
|
5125
|
+
if (refunded?.status !== "refunded" || !Number.isSafeInteger(refunded.refundedCents) || Number(refunded.refundedCents) !== Number(seat.amountCents)) {
|
|
5026
5126
|
throw new Error("named seat refund unconfirmed");
|
|
5027
5127
|
}
|
|
5028
5128
|
return { seatId: String(seat.id), status: "refunded", refundedCents: Number(refunded.refundedCents), subscriptionCanceled: false };
|
|
@@ -5044,6 +5144,7 @@ async function namedSeatOfferDigest(offer) {
|
|
|
5044
5144
|
])));
|
|
5045
5145
|
return [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
5046
5146
|
}
|
|
5147
|
+
var at = (value) => Number.isSafeInteger(value) ? Number(value) : void 0;
|
|
5047
5148
|
async function readNamedSeatOffer(db, input, now) {
|
|
5048
5149
|
membershipClock(now);
|
|
5049
5150
|
await loadMembershipAuthorityState(db, input);
|
|
@@ -5066,10 +5167,19 @@ async function readNamedSeatOffer(db, input, now) {
|
|
|
5066
5167
|
seatId: String(seat.id),
|
|
5067
5168
|
status: String(seat.status),
|
|
5068
5169
|
recipientName: String(seat.recipientName),
|
|
5069
|
-
recipientEmail: String(seat.recipientEmailNormalized)
|
|
5170
|
+
recipientEmail: String(seat.recipientEmailNormalized),
|
|
5171
|
+
recipientState: namedSeatRecipientState(seat),
|
|
5172
|
+
...at(seat.purchasedAt) !== void 0 ? { purchasedAt: at(seat.purchasedAt) } : {},
|
|
5173
|
+
...at(seat.acceptedAt) !== void 0 ? { acceptedAt: at(seat.acceptedAt) } : {},
|
|
5174
|
+
...seat.status === "active" && at(seat.journeyApprovedAt) !== void 0 ? { joinedAt: at(seat.journeyApprovedAt) } : {}
|
|
5070
5175
|
} } : {}
|
|
5071
5176
|
};
|
|
5072
|
-
return {
|
|
5177
|
+
return {
|
|
5178
|
+
...offer,
|
|
5179
|
+
digest: await namedSeatOfferDigest(offer),
|
|
5180
|
+
included: offering.seatPriceCents === 0,
|
|
5181
|
+
purchaseWindowed: offering.seatPurchaseWindowCalendarMonths !== void 0
|
|
5182
|
+
};
|
|
5073
5183
|
}
|
|
5074
5184
|
|
|
5075
5185
|
// src/membership-seat-invitation.ts
|
|
@@ -5195,6 +5305,15 @@ async function handleMembershipSeatAction(action, db, peer, input, now, stripeSe
|
|
|
5195
5305
|
};
|
|
5196
5306
|
if (action === "seat-accept") return acceptNamedSeatRecipient(db, recipientInput, now);
|
|
5197
5307
|
if (action === "seat-approve") return approveNamedSeatRecipient(db, recipientInput, now);
|
|
5308
|
+
const checkout = {
|
|
5309
|
+
...target,
|
|
5310
|
+
priceId: peer.seatPriceId,
|
|
5311
|
+
recipientName: String(input.recipientName),
|
|
5312
|
+
recipientEmail: String(input.recipientEmail),
|
|
5313
|
+
purchaserEmail: String(input.purchaserEmail),
|
|
5314
|
+
purchaserUserId: String(input.purchaserUserId)
|
|
5315
|
+
};
|
|
5316
|
+
if (action === "seat-checkouts" && namedSeatIncluded(await reserveNamedMembershipSeat(db, checkout, now))) return grantIncludedNamedSeat(db, checkout, now);
|
|
5198
5317
|
const secret = await getVaultSecret(db, stripeSecretName);
|
|
5199
5318
|
if (!secret) throw new Error("network Stripe provider is unavailable");
|
|
5200
5319
|
if (action === "seat-decline") {
|
|
@@ -5210,14 +5329,7 @@ async function handleMembershipSeatAction(action, db, peer, input, now, stripeSe
|
|
|
5210
5329
|
return { seatId: seat.id, status: "refunded", refundedCents: refunded.refundedCents, subscriptionCanceled: false };
|
|
5211
5330
|
}
|
|
5212
5331
|
if (action === "seat-operator-refund") return refundNamedSeatByOperator(db, { ...target, seatId: String(input.seatId) }, secret, now);
|
|
5213
|
-
if (action === "seat-checkouts") return prepareNamedMembershipSeatPayment(db,
|
|
5214
|
-
...target,
|
|
5215
|
-
priceId: peer.seatPriceId,
|
|
5216
|
-
recipientName: String(input.recipientName),
|
|
5217
|
-
recipientEmail: String(input.recipientEmail),
|
|
5218
|
-
purchaserEmail: String(input.purchaserEmail),
|
|
5219
|
-
purchaserUserId: String(input.purchaserUserId)
|
|
5220
|
-
}, secret, now);
|
|
5332
|
+
if (action === "seat-checkouts") return prepareNamedMembershipSeatPayment(db, checkout, secret, now);
|
|
5221
5333
|
const evidence = { ...target, seatId: String(input.seatId), eventId: String(input.eventId) };
|
|
5222
5334
|
if (action === "seat-payments") {
|
|
5223
5335
|
const receipt = await confirmNamedMembershipSeatPayment(db, evidence, now);
|
|
@@ -5528,13 +5640,13 @@ async function requestNamedSeatDecline(options) {
|
|
|
5528
5640
|
verifiedRecipientEmail: options.verifiedRecipientEmail
|
|
5529
5641
|
});
|
|
5530
5642
|
const seat = result.seat;
|
|
5531
|
-
if (!seat || seat.seatId !== options.seatId || seat.status !== "refunded" || seat.subscriptionCanceled !== false || !Number.isSafeInteger(seat.refundedCents) || seat.refundedCents
|
|
5643
|
+
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");
|
|
5532
5644
|
return seat;
|
|
5533
5645
|
}
|
|
5534
5646
|
async function requestNamedSeatOperatorRefund(options) {
|
|
5535
5647
|
const result = await requestAuthority(options, "seat-operator-refund", { applicationId: options.applicationId, skuId: options.skuId, seatId: options.seatId });
|
|
5536
5648
|
const seat = result.seat;
|
|
5537
|
-
if (!seat || seat.seatId !== options.seatId || seat.status !== "refunded" || seat.subscriptionCanceled !== false || !Number.isSafeInteger(seat.refundedCents) || seat.refundedCents
|
|
5649
|
+
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");
|
|
5538
5650
|
return seat;
|
|
5539
5651
|
}
|
|
5540
5652
|
async function requestNamedSeatReservation(options) {
|
|
@@ -5548,13 +5660,13 @@ async function requestNamedSeatReservation(options) {
|
|
|
5548
5660
|
});
|
|
5549
5661
|
const seat = result.seat;
|
|
5550
5662
|
const expected = (await membershipOfferIdForRequest({ ...options, requestId: "named_seat" })).replace(/^offer_/, "seat_");
|
|
5551
|
-
if (!seat || seat.seatId !== expected || !Number.isSafeInteger(seat.amountCents) || seat.amountCents
|
|
5663
|
+
if (!seat || seat.seatId !== expected || !Number.isSafeInteger(seat.amountCents) || seat.amountCents < 0 || !/^[a-z]{3}$/.test(seat.currency) || !Number.isSafeInteger(seat.termEndsAt)) throw new Error("invalid seat reservation receipt");
|
|
5552
5664
|
return { seatId: seat.seatId, amountCents: seat.amountCents, currency: seat.currency, termEndsAt: seat.termEndsAt };
|
|
5553
5665
|
}
|
|
5554
5666
|
async function requestNamedSeatOffer(options) {
|
|
5555
5667
|
const result = await requestAuthority(options, "seat-offer", { applicationId: options.applicationId, skuId: options.skuId });
|
|
5556
5668
|
const offer = result.seat;
|
|
5557
|
-
if (!offer || offer.membershipId !== await networkMembershipId(options) || !Number.isSafeInteger(offer.amountCents) || offer.amountCents
|
|
5669
|
+
if (!offer || offer.membershipId !== await networkMembershipId(options) || !Number.isSafeInteger(offer.amountCents) || offer.amountCents < 0 || offer.included !== (offer.amountCents === 0) || !/^[a-z]{3}$/.test(offer.currency) || !["month", "year"].includes(offer.interval) || typeof offer.eligible !== "boolean" || typeof offer.autoRenew !== "boolean" || !Number.isSafeInteger(offer.termEndsAt) || !Number.isSafeInteger(offer.purchaseEndsAt) || offer.purchaseWindowed !== void 0 && typeof offer.purchaseWindowed !== "boolean" || offer.digest !== await namedSeatOfferDigest(offer)) throw new Error("invalid named seat offer");
|
|
5558
5670
|
return offer;
|
|
5559
5671
|
}
|
|
5560
5672
|
async function requestNamedSeatInvitation(options) {
|
|
@@ -5590,7 +5702,8 @@ async function requestNamedSeatCheckout(options) {
|
|
|
5590
5702
|
});
|
|
5591
5703
|
const seat = result.seat;
|
|
5592
5704
|
const expected = (await membershipOfferIdForRequest({ ...options, requestId: "named_seat" })).replace(/^offer_/, "seat_");
|
|
5593
|
-
|
|
5705
|
+
const included = seat?.amountCents === 0;
|
|
5706
|
+
if (!seat || seat.seatId !== expected || !Number.isSafeInteger(seat.amountCents) || seat.amountCents < 0 || !/^[a-z]{3}$/.test(seat.currency) || !Number.isSafeInteger(seat.termEndsAt) || (included ? seat.included !== true || seat.paymentIntentId !== void 0 || seat.clientSecret !== void 0 || !["pending_acceptance", "pending_approval", "pending_primary", "active"].includes(seat.status) : seat.included !== void 0 || !/^pi_[A-Za-z0-9_]+$/.test(String(seat.paymentIntentId)) || typeof seat.clientSecret !== "string" || !["payment_processing", "renewal_pending", "pending_acceptance", "pending_approval", "active"].includes(seat.status))) {
|
|
5594
5707
|
throw new Error("invalid named seat checkout receipt");
|
|
5595
5708
|
}
|
|
5596
5709
|
return seat;
|