@stewardhq/sdk 0.2.0 → 0.5.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 +123 -88
- package/dist/_chunks/errors.js +1 -1
- package/dist/_chunks/events.d.ts +315 -4
- package/dist/_chunks/events.js +1155 -9
- package/dist/_chunks/index.d.ts +3473 -90
- package/dist/_chunks/locale.d.ts +269 -1
- package/dist/_chunks/src.js +842 -24
- package/dist/_chunks/validators.d.ts +45 -1
- package/dist/_chunks/validators.js +128 -27
- package/dist/contract.d.ts +4 -4
- package/dist/contract.js +4 -4
- package/dist/index.d.ts +436 -22
- package/dist/index.js +1659 -26
- package/dist/server.d.ts +44 -6
- package/dist/server.js +29 -2
- package/package.json +4 -10
- package/dist/_chunks/steward.d.ts +0 -189
- package/dist/_chunks/steward.js +0 -560
- package/dist/testing/fixtures/events/LOCK.json +0 -27
- package/dist/testing/fixtures/events/account.deleted.json +0 -36
- package/dist/testing/fixtures/events/account.state_changed.json +0 -53
- package/dist/testing/fixtures/events/account.updated.json +0 -37
- package/dist/testing/fixtures/events/checkout.completed.json +0 -36
- package/dist/testing/fixtures/events/checkout.expired.json +0 -26
- package/dist/testing/fixtures/events/checkout.failed.json +0 -27
- package/dist/testing/fixtures/events/invoice.created.json +0 -37
- package/dist/testing/fixtures/events/invoice.issued.json +0 -38
- package/dist/testing/fixtures/events/invoice.voided.json +0 -39
- package/dist/testing/fixtures/events/subscription.activated.json +0 -40
- package/dist/testing/fixtures/events/subscription.cancel_scheduled.json +0 -38
- package/dist/testing/fixtures/events/subscription.canceled.json +0 -29
- package/dist/testing/fixtures/events/subscription.expired.json +0 -27
- package/dist/testing/fixtures/events/subscription.payment_failed.json +0 -38
- package/dist/testing/fixtures/events/subscription.reactivated.json +0 -36
- package/dist/testing/fixtures/events/subscription.renewed.json +0 -39
- package/dist/testing/fixtures/events/subscription.suspended.json +0 -36
- package/dist/testing/fixtures/events/subscription.terminated.json +0 -28
- package/dist/testing.d.ts +0 -626
- package/dist/testing.js +0 -3638
package/dist/testing.js
DELETED
|
@@ -1,3638 +0,0 @@
|
|
|
1
|
-
import { C as BillingProfileSchema, D as API_VERSION, O as ActorRefSchema, R as ExternalRefSchema, a as EVENT_TYPES, d as ACCOUNT_INCLUDES, i as EVENT_DETAIL_SCHEMAS, r as BillingEventSchema, s as LEGACY_EVENT_TYPES, x as AccountUpsertInputSchema, z as IdSchema } from "./_chunks/events.js";
|
|
2
|
-
import { $ as InvoiceIssueInputSchema, A as webhookEndpointUrlProblem, Bt as applySettingsPatch, G as HEADER_ACTOR_REF, Gt as CatalogSyncInputSchema, J as HEADER_REQUEST_ID, L as ERROR_CODES, M as PlanSetInputSchema, Nt as ProductSettingsPatchSchema, Ot as checkoutRedirectUrl, Pt as ProductSettingsSchema, S as WebhookEndpointCreateInputSchema, Vt as isReturnUrlAllowed, at as ManualInvoiceInputSchema, ct as splitInclusiveTax, gt as PortalSessionCreateInputSchema, h as buildDoctorReport, lt as CancelSubscriptionInputSchema, q as HEADER_IDEMPOTENCY_KEY, rt as InvoiceVoidInputSchema, st as manualInvoiceTotals, ut as GrantCreateInputSchema, v as DEFAULT_ENDPOINT_EVENT_TYPES, w as WebhookEndpointRotateInputSchema, xt as CheckoutSessionCreateInputSchema } from "./_chunks/src.js";
|
|
3
|
-
import { i as LIVE_SUBSCRIPTION_STATUSES, r as isValidVkn, s as resolveEntitlements, t as isE164 } from "./_chunks/validators.js";
|
|
4
|
-
import { t as createSteward } from "./_chunks/steward.js";
|
|
5
|
-
import { n as signWebhook } from "./_chunks/webhook.js";
|
|
6
|
-
import { createHash } from "node:crypto";
|
|
7
|
-
//#region src/testing/world.ts
|
|
8
|
-
const DAY_MS = 864e5;
|
|
9
|
-
/** Servisin `DEFAULT_DUNNING_POLICY` kopyası (`domain/dunning-policy.ts`). */
|
|
10
|
-
const DEFAULT_DUNNING_POLICY = {
|
|
11
|
-
steps: [
|
|
12
|
-
{
|
|
13
|
-
afterDays: 1,
|
|
14
|
-
action: "retry_payment"
|
|
15
|
-
},
|
|
16
|
-
{
|
|
17
|
-
afterDays: 3,
|
|
18
|
-
action: "retry_payment"
|
|
19
|
-
},
|
|
20
|
-
{
|
|
21
|
-
afterDays: 7,
|
|
22
|
-
action: "suspend"
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
afterDays: 30,
|
|
26
|
-
action: "terminate"
|
|
27
|
-
}
|
|
28
|
-
],
|
|
29
|
-
dryRun: true
|
|
30
|
-
};
|
|
31
|
-
function createWorld(options) {
|
|
32
|
-
let offset = 0;
|
|
33
|
-
const now = () => options.now().getTime() + offset;
|
|
34
|
-
const id = uuidv7Factory(now);
|
|
35
|
-
return {
|
|
36
|
-
productCode: options.productCode,
|
|
37
|
-
settings: options.settings,
|
|
38
|
-
permissiveReturnOrigins: options.permissiveReturnOrigins,
|
|
39
|
-
adminEndpoints: [],
|
|
40
|
-
dunningPolicy: options.dunningPolicy,
|
|
41
|
-
endpoint: options.endpointTypes === null ? null : {
|
|
42
|
-
id: id(),
|
|
43
|
-
types: new Set(options.endpointTypes)
|
|
44
|
-
},
|
|
45
|
-
features: [],
|
|
46
|
-
plans: /* @__PURE__ */ new Map(),
|
|
47
|
-
prices: [],
|
|
48
|
-
defaultPlanCode: null,
|
|
49
|
-
accounts: /* @__PURE__ */ new Map(),
|
|
50
|
-
subscriptions: [],
|
|
51
|
-
sessions: [],
|
|
52
|
-
portalSessions: [],
|
|
53
|
-
payments: [],
|
|
54
|
-
invoices: [],
|
|
55
|
-
grants: [],
|
|
56
|
-
events: [],
|
|
57
|
-
now,
|
|
58
|
-
setNow: (ms) => {
|
|
59
|
-
const current = now();
|
|
60
|
-
if (ms > current) offset += ms - current;
|
|
61
|
-
},
|
|
62
|
-
id
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
const iso = (ms) => new Date(ms).toISOString();
|
|
66
|
-
const isoOrNull = (ms) => ms === null ? null : iso(ms);
|
|
67
|
-
function sha256Hex(data) {
|
|
68
|
-
return createHash("sha256").update(data).digest("hex");
|
|
69
|
-
}
|
|
70
|
-
const compare = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
71
|
-
/**
|
|
72
|
-
* Dönem ekleme, UTC; ay sonu sabitlenir (31 Ocak + 1 ay = 28/29 Şubat) — servisin
|
|
73
|
-
* `periods.ts` `addInterval`'ı (date-fns + UTCDate) ile aynı sonuç, süreç saat diliminden bağımsız.
|
|
74
|
-
*/
|
|
75
|
-
function addInterval(from, interval, count = 1) {
|
|
76
|
-
const d = new Date(from);
|
|
77
|
-
const months = d.getUTCMonth() + (interval === "year" ? 12 * count : count);
|
|
78
|
-
const lastDay = new Date(Date.UTC(d.getUTCFullYear(), months + 1, 0)).getUTCDate();
|
|
79
|
-
return Date.UTC(d.getUTCFullYear(), months, Math.min(d.getUTCDate(), lastDay), d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds(), d.getUTCMilliseconds());
|
|
80
|
-
}
|
|
81
|
-
function sortDeep(value) {
|
|
82
|
-
if (Array.isArray(value)) return value.map(sortDeep);
|
|
83
|
-
if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).sort(([a], [b]) => compare(a, b)).map(([k, v]) => [k, sortDeep(v)]));
|
|
84
|
-
return value;
|
|
85
|
-
}
|
|
86
|
-
/** Anahtar sırasından bağımsız metin. */
|
|
87
|
-
const canonicalJson = (value) => JSON.stringify(sortDeep(value));
|
|
88
|
-
/** RFC 9562 UUIDv7: 48 bit ms + 12 bit sayaç (aynı ms'de artar) + rastgele. */
|
|
89
|
-
function uuidv7Factory(clock) {
|
|
90
|
-
let lastMs = 0;
|
|
91
|
-
let counter = 0;
|
|
92
|
-
return () => {
|
|
93
|
-
let ms = clock();
|
|
94
|
-
if (ms <= lastMs) {
|
|
95
|
-
ms = lastMs;
|
|
96
|
-
if (++counter > 4095) {
|
|
97
|
-
ms = lastMs + 1;
|
|
98
|
-
counter = 0;
|
|
99
|
-
}
|
|
100
|
-
} else counter = 0;
|
|
101
|
-
lastMs = ms;
|
|
102
|
-
const bytes = globalThis.crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(16));
|
|
103
|
-
for (let i = 0; i < 6; i++) bytes[i] = Math.floor(ms / 2 ** (8 * (5 - i))) & 255;
|
|
104
|
-
bytes[6] = 112 | counter >> 8 & 15;
|
|
105
|
-
bytes[7] = counter & 255;
|
|
106
|
-
bytes[8] = bytes[8] & 63 | 128;
|
|
107
|
-
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
108
|
-
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
/** `GET /v1/catalog` gövdesi (servisin `getCatalog` sırası); senkron yoksa null. */
|
|
112
|
-
function catalogOf(w) {
|
|
113
|
-
if (w.defaultPlanCode === null) return null;
|
|
114
|
-
const plans = [...w.plans.values()].filter((p) => p.active).sort((a, b) => a.rank - b.rank || compare(a.code, b.code));
|
|
115
|
-
const rank = new Map(plans.map((p) => [p.code, p.rank]));
|
|
116
|
-
const prices = w.prices.filter((p) => p.active && rank.has(p.planCode)).sort((a, b) => rank.get(a.planCode) - rank.get(b.planCode) || compare(a.interval, b.interval) || a.intervalCount - b.intervalCount || compare(a.currency, b.currency));
|
|
117
|
-
return {
|
|
118
|
-
product: w.productCode,
|
|
119
|
-
defaultPlanCode: w.defaultPlanCode,
|
|
120
|
-
features: [...w.features].sort((a, b) => compare(a.key, b.key)).map((f) => ({
|
|
121
|
-
key: f.key,
|
|
122
|
-
type: f.type,
|
|
123
|
-
...f.description !== void 0 ? { description: f.description } : {}
|
|
124
|
-
})),
|
|
125
|
-
plans: plans.map((p) => ({
|
|
126
|
-
code: p.code,
|
|
127
|
-
name: p.name,
|
|
128
|
-
rank: p.rank,
|
|
129
|
-
sellable: p.sellable,
|
|
130
|
-
entitlements: Object.fromEntries(Object.entries(p.entitlements).sort(([a], [b]) => compare(a, b)))
|
|
131
|
-
})),
|
|
132
|
-
prices: prices.map((p) => ({
|
|
133
|
-
id: p.id,
|
|
134
|
-
planCode: p.planCode,
|
|
135
|
-
interval: p.interval,
|
|
136
|
-
intervalCount: p.intervalCount,
|
|
137
|
-
currency: p.currency,
|
|
138
|
-
amountMinor: p.amountMinor,
|
|
139
|
-
taxInclusive: p.taxInclusive,
|
|
140
|
-
taxRateBps: p.taxRateBps,
|
|
141
|
-
active: p.active,
|
|
142
|
-
createdAt: iso(p.createdAt)
|
|
143
|
-
}))
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
/** `GET /v1/catalog` ETag'inin tırnaksız değeri ve `AccountState.catalogVersion`. */
|
|
147
|
-
const catalogVersionOf = (catalog) => sha256Hex(JSON.stringify(catalog)).slice(0, 32);
|
|
148
|
-
function priceOf(w, priceId) {
|
|
149
|
-
const price = w.prices.find((p) => p.id === priceId);
|
|
150
|
-
if (!price) throw new Error(`sahte steward: fiyat yok ${priceId}`);
|
|
151
|
-
return price;
|
|
152
|
-
}
|
|
153
|
-
const LIVE = LIVE_SUBSCRIPTION_STATUSES;
|
|
154
|
-
const DUNNING = ["past_due", "suspended"];
|
|
155
|
-
const isLive = (sub) => LIVE.includes(sub.status);
|
|
156
|
-
const inDunning = (sub) => DUNNING.includes(sub.status);
|
|
157
|
-
function liveSubscription(w, ref) {
|
|
158
|
-
return w.subscriptions.find((s) => s.accountRef === ref && isLive(s)) ?? null;
|
|
159
|
-
}
|
|
160
|
-
/** Servisin `dunningStateOf`'u: sıradaki aksiyon politikadan (başlangıç + gün). */
|
|
161
|
-
function dunningStateOf(policy, sub) {
|
|
162
|
-
const next = policy.steps[sub.dunningStep];
|
|
163
|
-
return {
|
|
164
|
-
step: sub.dunningStep,
|
|
165
|
-
nextActionAt: next && sub.dunningStartedAt !== null ? iso(sub.dunningStartedAt + next.afterDays * DAY_MS) : null
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
function computeState(w, account, now) {
|
|
169
|
-
const catalog = catalogOf(w);
|
|
170
|
-
if (!catalog) throw new Error(`ürünün varsayılan planı yok (katalog senkronlanmadı): hesap ${account.ref}`);
|
|
171
|
-
const live = liveSubscription(w, account.ref);
|
|
172
|
-
const livePrice = live ? priceOf(w, live.priceId) : null;
|
|
173
|
-
const grants = w.grants.filter((g) => g.accountRef === account.ref && g.revokedAt === null && g.startsAt <= now && (g.endsAt === null || g.endsAt > now)).sort((a, b) => a.createdAt - b.createdAt || compare(a.id, b.id));
|
|
174
|
-
const listed = new Set(catalog.plans.map((p) => p.code));
|
|
175
|
-
const retired = [.../* @__PURE__ */ new Set([livePrice?.planCode, ...grants.map((g) => g.planCode)])].filter((code) => typeof code === "string" && !listed.has(code)).flatMap((code) => {
|
|
176
|
-
const plan = w.plans.get(code);
|
|
177
|
-
return plan ? [{
|
|
178
|
-
code: plan.code,
|
|
179
|
-
rank: plan.rank,
|
|
180
|
-
entitlements: plan.entitlements
|
|
181
|
-
}] : [];
|
|
182
|
-
});
|
|
183
|
-
const resolved = resolveEntitlements({
|
|
184
|
-
catalog: {
|
|
185
|
-
defaultPlanCode: catalog.defaultPlanCode,
|
|
186
|
-
features: catalog.features,
|
|
187
|
-
plans: [...catalog.plans, ...retired]
|
|
188
|
-
},
|
|
189
|
-
subscription: live && livePrice ? {
|
|
190
|
-
planCode: livePrice.planCode,
|
|
191
|
-
status: live.status
|
|
192
|
-
} : null,
|
|
193
|
-
grants: grants.map((g) => ({
|
|
194
|
-
planCode: g.planCode,
|
|
195
|
-
overrides: g.overrides
|
|
196
|
-
}))
|
|
197
|
-
});
|
|
198
|
-
const open = w.sessions.filter((s) => s.accountRef === account.ref && s.status === "open" && s.expiresAt > now).sort((a, b) => b.createdAt - a.createdAt || compare(b.id, a.id))[0];
|
|
199
|
-
const openPrice = open ? priceOf(w, open.priceId) : null;
|
|
200
|
-
const dunning = live !== null && inDunning(live);
|
|
201
|
-
return {
|
|
202
|
-
ref: account.ref,
|
|
203
|
-
displayName: account.displayName,
|
|
204
|
-
updatedAt: iso(now),
|
|
205
|
-
catalogVersion: catalogVersionOf(catalog),
|
|
206
|
-
...resolved,
|
|
207
|
-
subscription: live && livePrice ? {
|
|
208
|
-
id: live.id,
|
|
209
|
-
status: live.status,
|
|
210
|
-
planCode: livePrice.planCode,
|
|
211
|
-
interval: livePrice.interval,
|
|
212
|
-
amountMinor: livePrice.amountMinor,
|
|
213
|
-
currency: livePrice.currency,
|
|
214
|
-
currentPeriodEnd: isoOrNull(live.currentPeriodEnd),
|
|
215
|
-
cancelAt: isoOrNull(live.cancelAt),
|
|
216
|
-
cancelAtPeriodEnd: live.cancelAtPeriodEnd,
|
|
217
|
-
pastDueSince: dunning ? isoOrNull(live.pastDueSince ?? live.dunningStartedAt) : null,
|
|
218
|
-
currentPeriodStart: isoOrNull(live.currentPeriodStart),
|
|
219
|
-
dunning: dunning ? dunningStateOf(w.dunningPolicy, live) : null,
|
|
220
|
-
scheduledChange: null
|
|
221
|
-
} : null,
|
|
222
|
-
openCheckout: open && openPrice ? {
|
|
223
|
-
id: open.id,
|
|
224
|
-
planCode: openPrice.planCode,
|
|
225
|
-
interval: openPrice.interval,
|
|
226
|
-
intervalCount: openPrice.intervalCount,
|
|
227
|
-
currency: openPrice.currency,
|
|
228
|
-
amountMinor: openPrice.amountMinor,
|
|
229
|
-
expiresAt: iso(open.expiresAt)
|
|
230
|
-
} : null,
|
|
231
|
-
profile: {
|
|
232
|
-
present: account.profile !== null,
|
|
233
|
-
kind: account.profile?.kind ?? null
|
|
234
|
-
}
|
|
235
|
-
};
|
|
236
|
-
}
|
|
237
|
-
/** Durumun eski (0.1) snapshot izdüşümü. */
|
|
238
|
-
function toSnapshot(state) {
|
|
239
|
-
const sub = state.subscription;
|
|
240
|
-
return {
|
|
241
|
-
version: state.version,
|
|
242
|
-
planCode: state.planCode,
|
|
243
|
-
accessState: state.accessState,
|
|
244
|
-
entitlements: state.entitlements,
|
|
245
|
-
subscription: sub ? {
|
|
246
|
-
id: sub.id,
|
|
247
|
-
status: sub.status,
|
|
248
|
-
planCode: sub.planCode,
|
|
249
|
-
interval: sub.interval,
|
|
250
|
-
amountMinor: sub.amountMinor,
|
|
251
|
-
currency: sub.currency,
|
|
252
|
-
currentPeriodEnd: sub.currentPeriodEnd,
|
|
253
|
-
cancelAt: sub.cancelAt
|
|
254
|
-
} : null
|
|
255
|
-
};
|
|
256
|
-
}
|
|
257
|
-
const NOT_COMPARED = /* @__PURE__ */ new Set([
|
|
258
|
-
"version",
|
|
259
|
-
"updatedAt",
|
|
260
|
-
"catalogVersion"
|
|
261
|
-
]);
|
|
262
|
-
const fingerprint = (state) => canonicalJson(Object.fromEntries(Object.entries(state).filter(([k]) => !NOT_COMPARED.has(k))));
|
|
263
|
-
/** Sözleşmeye uyan zarf; uymuyorsa fırlatır (servis transaction'ı düşürür). */
|
|
264
|
-
function envelope(w, ref, state, event, id, createdAt) {
|
|
265
|
-
const payload = {
|
|
266
|
-
id,
|
|
267
|
-
type: event.type,
|
|
268
|
-
apiVersion: API_VERSION,
|
|
269
|
-
createdAt: iso(createdAt),
|
|
270
|
-
product: w.productCode,
|
|
271
|
-
accountRef: ref,
|
|
272
|
-
data: {
|
|
273
|
-
account: state,
|
|
274
|
-
detail: event.detail
|
|
275
|
-
}
|
|
276
|
-
};
|
|
277
|
-
const parsed = BillingEventSchema.safeParse(payload);
|
|
278
|
-
if (!parsed.success) throw new Error(`sözleşmeye uymayan event (${event.type}): ${parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}`);
|
|
279
|
-
return payload;
|
|
280
|
-
}
|
|
281
|
-
function emitEvent(w, ref, state, event) {
|
|
282
|
-
const now = w.now();
|
|
283
|
-
const id = `evt_${w.id()}`;
|
|
284
|
-
const payload = envelope(w, ref, state, event, id, now);
|
|
285
|
-
const wanted = w.endpoint !== null && w.endpoint.types.has(event.type);
|
|
286
|
-
w.events.push({
|
|
287
|
-
id,
|
|
288
|
-
accountRef: ref,
|
|
289
|
-
type: event.type,
|
|
290
|
-
payload,
|
|
291
|
-
delivery: wanted ? {
|
|
292
|
-
status: "pending",
|
|
293
|
-
attempts: 0,
|
|
294
|
-
lastHttpStatus: null,
|
|
295
|
-
lastError: null,
|
|
296
|
-
nextAttemptAt: now,
|
|
297
|
-
deliveredAt: null
|
|
298
|
-
} : null
|
|
299
|
-
});
|
|
300
|
-
return id;
|
|
301
|
-
}
|
|
302
|
-
const SNAPSHOT_KEYS = [
|
|
303
|
-
"planCode",
|
|
304
|
-
"accessState",
|
|
305
|
-
"entitlements",
|
|
306
|
-
"subscription"
|
|
307
|
-
];
|
|
308
|
-
function account(w, ref) {
|
|
309
|
-
const row = w.accounts.get(ref);
|
|
310
|
-
if (!row) throw new Error(`sahte steward: hesap yok ${ref}`);
|
|
311
|
-
return row;
|
|
312
|
-
}
|
|
313
|
-
/** Saklı durum (`readAccountState`); hesap henüz commit edilmediyse hesaplanır. */
|
|
314
|
-
function storedState(w, ref) {
|
|
315
|
-
return account(w, ref).state ?? commitAccountChange(w, ref, "account.refreshed").state;
|
|
316
|
-
}
|
|
317
|
-
function commitAccountChange(w, ref, change) {
|
|
318
|
-
const row = account(w, ref);
|
|
319
|
-
const previous = row.state;
|
|
320
|
-
const next = computeState(w, row, w.now());
|
|
321
|
-
const changed = previous === null || fingerprint(previous) !== fingerprint(next);
|
|
322
|
-
const state = changed ? {
|
|
323
|
-
version: row.version + 1,
|
|
324
|
-
...next
|
|
325
|
-
} : previous;
|
|
326
|
-
if (changed) {
|
|
327
|
-
row.version = state.version;
|
|
328
|
-
row.state = state;
|
|
329
|
-
row.history.push(state);
|
|
330
|
-
}
|
|
331
|
-
const primary = typeof change === "string" ? null : change;
|
|
332
|
-
let eventId = null;
|
|
333
|
-
if (primary) eventId = emitEvent(w, ref, state, primary);
|
|
334
|
-
else if (changed && previous) {
|
|
335
|
-
const before = toSnapshot(previous);
|
|
336
|
-
const after = toSnapshot(state);
|
|
337
|
-
const keys = SNAPSHOT_KEYS.filter((k) => canonicalJson(before[k]) !== canonicalJson(after[k]));
|
|
338
|
-
if (keys.length > 0) eventId = emitEvent(w, ref, state, {
|
|
339
|
-
type: "account.updated",
|
|
340
|
-
detail: { changed: [...keys] }
|
|
341
|
-
});
|
|
342
|
-
}
|
|
343
|
-
let stateEventId = null;
|
|
344
|
-
if (changed && previous) stateEventId = emitEvent(w, ref, state, {
|
|
345
|
-
type: "account.state_changed",
|
|
346
|
-
detail: {
|
|
347
|
-
cause: primary?.type ?? change,
|
|
348
|
-
causeEventId: primary ? eventId : null
|
|
349
|
-
}
|
|
350
|
-
});
|
|
351
|
-
return {
|
|
352
|
-
state,
|
|
353
|
-
changed,
|
|
354
|
-
eventId,
|
|
355
|
-
stateEventId
|
|
356
|
-
};
|
|
357
|
-
}
|
|
358
|
-
/**
|
|
359
|
-
* Servisin `republishState`'i (`domain/outbox.ts`): verilen (saklı) durumu
|
|
360
|
-
* `account.state_changed` (`cause: "resync"`) olarak yazar; version artmaz, eski
|
|
361
|
-
* tip yok, teslimat filtre kuralıyla. Event id'si döner.
|
|
362
|
-
*/
|
|
363
|
-
function republishState(w, ref, state) {
|
|
364
|
-
return emitEvent(w, ref, state, {
|
|
365
|
-
type: "account.state_changed",
|
|
366
|
-
detail: {
|
|
367
|
-
cause: "resync",
|
|
368
|
-
causeEventId: null
|
|
369
|
-
}
|
|
370
|
-
});
|
|
371
|
-
}
|
|
372
|
-
function toSubscription(w, row) {
|
|
373
|
-
const price = priceOf(w, row.priceId);
|
|
374
|
-
return {
|
|
375
|
-
id: row.id,
|
|
376
|
-
accountRef: row.accountRef,
|
|
377
|
-
status: row.status,
|
|
378
|
-
planCode: price.planCode,
|
|
379
|
-
priceId: row.priceId,
|
|
380
|
-
interval: price.interval,
|
|
381
|
-
amountMinor: price.amountMinor,
|
|
382
|
-
currency: price.currency,
|
|
383
|
-
provider: "fake",
|
|
384
|
-
providerSubscriptionRef: row.providerSubscriptionRef,
|
|
385
|
-
cancelAtPeriodEnd: row.cancelAtPeriodEnd,
|
|
386
|
-
cancelAt: isoOrNull(row.cancelAt),
|
|
387
|
-
cancelReason: row.cancelReason,
|
|
388
|
-
canceledAt: isoOrNull(row.canceledAt),
|
|
389
|
-
startedAt: iso(row.startedAt),
|
|
390
|
-
currentPeriodStart: isoOrNull(row.currentPeriodStart),
|
|
391
|
-
currentPeriodEnd: isoOrNull(row.currentPeriodEnd),
|
|
392
|
-
dunningStartedAt: isoOrNull(row.dunningStartedAt)
|
|
393
|
-
};
|
|
394
|
-
}
|
|
395
|
-
function toCheckoutSession(w, row) {
|
|
396
|
-
const price = priceOf(w, row.priceId);
|
|
397
|
-
return {
|
|
398
|
-
id: row.id,
|
|
399
|
-
accountRef: row.accountRef,
|
|
400
|
-
status: row.status,
|
|
401
|
-
planCode: price.planCode,
|
|
402
|
-
interval: price.interval,
|
|
403
|
-
amountMinor: price.amountMinor,
|
|
404
|
-
currency: price.currency,
|
|
405
|
-
provider: "fake",
|
|
406
|
-
locale: row.locale,
|
|
407
|
-
successUrl: row.successUrl,
|
|
408
|
-
cancelUrl: row.cancelUrl,
|
|
409
|
-
consents: row.consents,
|
|
410
|
-
actorRef: row.actorRef,
|
|
411
|
-
failureReason: row.failureReason,
|
|
412
|
-
subscriptionId: row.subscriptionId,
|
|
413
|
-
expiresAt: iso(row.expiresAt),
|
|
414
|
-
createdAt: iso(row.createdAt),
|
|
415
|
-
completedAt: isoOrNull(row.completedAt)
|
|
416
|
-
};
|
|
417
|
-
}
|
|
418
|
-
function toGrant(row) {
|
|
419
|
-
return {
|
|
420
|
-
id: row.id,
|
|
421
|
-
accountRef: row.accountRef,
|
|
422
|
-
planCode: row.planCode,
|
|
423
|
-
overrides: row.overrides,
|
|
424
|
-
reason: row.reason,
|
|
425
|
-
actorRef: row.actorRef,
|
|
426
|
-
startsAt: iso(row.startsAt),
|
|
427
|
-
endsAt: isoOrNull(row.endsAt),
|
|
428
|
-
revokedAt: isoOrNull(row.revokedAt),
|
|
429
|
-
createdAt: iso(row.createdAt)
|
|
430
|
-
};
|
|
431
|
-
}
|
|
432
|
-
function toInvoice(row) {
|
|
433
|
-
return {
|
|
434
|
-
id: row.id,
|
|
435
|
-
accountRef: row.accountRef,
|
|
436
|
-
subscriptionId: row.subscriptionId,
|
|
437
|
-
paymentId: row.paymentId,
|
|
438
|
-
status: row.status,
|
|
439
|
-
number: row.number,
|
|
440
|
-
currency: row.currency,
|
|
441
|
-
subtotalMinor: row.subtotalMinor,
|
|
442
|
-
taxMinor: row.taxMinor,
|
|
443
|
-
totalMinor: row.totalMinor,
|
|
444
|
-
taxRateBps: row.taxRateBps,
|
|
445
|
-
buyer: row.buyer,
|
|
446
|
-
lines: row.lines,
|
|
447
|
-
issuer: "manual",
|
|
448
|
-
externalRef: row.externalRef,
|
|
449
|
-
hasDocument: row.document !== null,
|
|
450
|
-
issuedAt: isoOrNull(row.issuedAt),
|
|
451
|
-
createdAt: iso(row.createdAt),
|
|
452
|
-
dueAt: isoOrNull(row.dueAt ?? null),
|
|
453
|
-
note: row.note ?? null,
|
|
454
|
-
voidedAt: isoOrNull(row.voidedAt ?? null),
|
|
455
|
-
voidReason: row.voidReason ?? null
|
|
456
|
-
};
|
|
457
|
-
}
|
|
458
|
-
//#endregion
|
|
459
|
-
//#region src/testing/operations.ts
|
|
460
|
-
const ok = (value) => ({
|
|
461
|
-
ok: true,
|
|
462
|
-
value
|
|
463
|
-
});
|
|
464
|
-
const fail = (code, message) => message === void 0 ? {
|
|
465
|
-
ok: false,
|
|
466
|
-
code
|
|
467
|
-
} : {
|
|
468
|
-
ok: false,
|
|
469
|
-
code,
|
|
470
|
-
message
|
|
471
|
-
};
|
|
472
|
-
function matchesType(type, value) {
|
|
473
|
-
switch (type) {
|
|
474
|
-
case "limit": return value === null || typeof value === "number" && Number.isInteger(value) && value >= 0;
|
|
475
|
-
case "boolean": return typeof value === "boolean";
|
|
476
|
-
case "string": return typeof value === "string";
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
/** `syncCatalogWithProvider`: K4 tamlığı, farkın uygulanması, etkilenen hesapların yenilenmesi. */
|
|
480
|
-
function syncCatalog(w, input) {
|
|
481
|
-
const missing = [];
|
|
482
|
-
const mismatched = [];
|
|
483
|
-
for (const p of input.plans) for (const f of input.features) {
|
|
484
|
-
const value = p.entitlements[f.key];
|
|
485
|
-
if (value === void 0) missing.push(`plan ${p.code}: ${f.key} için değer yok`);
|
|
486
|
-
else if (!matchesType(f.type, value)) mismatched.push(`plan ${p.code}: ${f.key} değeri ${f.type} tipine uymuyor`);
|
|
487
|
-
}
|
|
488
|
-
if (missing.length > 0) return fail("plan_missing_feature", missing.slice(0, 10).join("; "));
|
|
489
|
-
if (mismatched.length > 0) return fail("override_type_mismatch", mismatched.slice(0, 10).join("; "));
|
|
490
|
-
const now = w.now();
|
|
491
|
-
const keys = new Set(input.features.map((f) => f.key));
|
|
492
|
-
const removedFeatures = w.features.filter((f) => !keys.has(f.key)).map((f) => f.key);
|
|
493
|
-
w.features = input.features.map((f) => ({
|
|
494
|
-
key: f.key,
|
|
495
|
-
type: f.type,
|
|
496
|
-
...f.description !== void 0 ? { description: f.description } : {}
|
|
497
|
-
}));
|
|
498
|
-
const canonical = (entitlements) => canonicalJson(entitlements);
|
|
499
|
-
const changedPlans = [];
|
|
500
|
-
for (const p of input.plans) {
|
|
501
|
-
const before = w.plans.get(p.code);
|
|
502
|
-
const planChanged = !before || !before.active || before.name !== p.name || before.rank !== p.rank || before.sellable !== p.sellable;
|
|
503
|
-
const entitlementsChanged = !before || canonical(before.entitlements) !== canonical(p.entitlements);
|
|
504
|
-
w.plans.set(p.code, {
|
|
505
|
-
code: p.code,
|
|
506
|
-
name: p.name,
|
|
507
|
-
rank: p.rank,
|
|
508
|
-
sellable: p.sellable,
|
|
509
|
-
active: true,
|
|
510
|
-
entitlements: entitlementsChanged ? { ...p.entitlements } : before.entitlements
|
|
511
|
-
});
|
|
512
|
-
if (planChanged || entitlementsChanged) changedPlans.push(p.code);
|
|
513
|
-
}
|
|
514
|
-
const inputPlans = new Set(input.plans.map((p) => p.code));
|
|
515
|
-
const retiredPlans = [];
|
|
516
|
-
for (const plan of w.plans.values()) if (plan.active && !inputPlans.has(plan.code)) {
|
|
517
|
-
plan.active = false;
|
|
518
|
-
retiredPlans.push(plan.code);
|
|
519
|
-
}
|
|
520
|
-
const defaultPlanChanged = w.defaultPlanCode !== input.defaultPlanCode;
|
|
521
|
-
w.defaultPlanCode = input.defaultPlanCode;
|
|
522
|
-
const priceKey = (p) => `${p.planCode}/${p.interval}/${p.intervalCount}/${p.currency}`;
|
|
523
|
-
const active = new Map(w.prices.filter((p) => p.active).map((p) => [priceKey(p), p]));
|
|
524
|
-
const keep = /* @__PURE__ */ new Set();
|
|
525
|
-
const createdPrices = [];
|
|
526
|
-
const deactivatedPrices = [];
|
|
527
|
-
for (const p of input.prices) {
|
|
528
|
-
const current = active.get(priceKey(p));
|
|
529
|
-
if (current && current.amountMinor === p.amountMinor && current.taxInclusive === p.taxInclusive && current.taxRateBps === p.taxRateBps) {
|
|
530
|
-
keep.add(current.id);
|
|
531
|
-
continue;
|
|
532
|
-
}
|
|
533
|
-
if (current) {
|
|
534
|
-
current.active = false;
|
|
535
|
-
deactivatedPrices.push(current.id);
|
|
536
|
-
}
|
|
537
|
-
const id = w.id();
|
|
538
|
-
w.prices.push({
|
|
539
|
-
id,
|
|
540
|
-
...p,
|
|
541
|
-
active: true,
|
|
542
|
-
createdAt: now
|
|
543
|
-
});
|
|
544
|
-
createdPrices.push(id);
|
|
545
|
-
keep.add(id);
|
|
546
|
-
}
|
|
547
|
-
for (const price of active.values()) if (price.active && !keep.has(price.id)) {
|
|
548
|
-
price.active = false;
|
|
549
|
-
deactivatedPrices.push(price.id);
|
|
550
|
-
}
|
|
551
|
-
const unchanged = createdPrices.length === 0 && deactivatedPrices.length === 0 && changedPlans.length === 0 && retiredPlans.length === 0 && removedFeatures.length === 0 && !defaultPlanChanged;
|
|
552
|
-
if (changedPlans.length > 0 || retiredPlans.length > 0 || removedFeatures.length > 0 || defaultPlanChanged) for (const ref of liveAccountRefs(w)) commitAccountChange(w, ref, "catalog.synced");
|
|
553
|
-
return ok({
|
|
554
|
-
unchanged,
|
|
555
|
-
createdPrices,
|
|
556
|
-
deactivatedPrices,
|
|
557
|
-
changedPlans,
|
|
558
|
-
retiredPlans,
|
|
559
|
-
removedFeatures,
|
|
560
|
-
defaultPlanChanged,
|
|
561
|
-
providerErrors: []
|
|
562
|
-
});
|
|
563
|
-
}
|
|
564
|
-
/** Silinmemiş hesaplar: toplu yenileme silinmiş hesabı atlar (servisin `refreshProductAccounts`'u). */
|
|
565
|
-
const liveAccountRefs = (w) => [...w.accounts.values()].filter((row) => row.deletedAt === void 0).map((row) => row.ref);
|
|
566
|
-
/** `refreshAccounts` (`POST /v1/admin/accounts/refresh`). */
|
|
567
|
-
function refreshAccounts(w) {
|
|
568
|
-
let changed = 0;
|
|
569
|
-
for (const ref of liveAccountRefs(w)) if (commitAccountChange(w, ref, "account.refreshed").changed) changed++;
|
|
570
|
-
return { changed };
|
|
571
|
-
}
|
|
572
|
-
function toAccount(row) {
|
|
573
|
-
return {
|
|
574
|
-
ref: row.ref,
|
|
575
|
-
displayName: row.displayName,
|
|
576
|
-
metadata: row.metadata,
|
|
577
|
-
createdAt: iso(row.createdAt),
|
|
578
|
-
snapshot: toSnapshot(stateOf(row))
|
|
579
|
-
};
|
|
580
|
-
}
|
|
581
|
-
function stateOf(row) {
|
|
582
|
-
if (!row.state) throw new Error(`sahte steward: hesap durumu yok ${row.ref}`);
|
|
583
|
-
return row.state;
|
|
584
|
-
}
|
|
585
|
-
function upsertAccount(w, ref, input) {
|
|
586
|
-
let row = w.accounts.get(ref);
|
|
587
|
-
const created = row === void 0;
|
|
588
|
-
if (!row) {
|
|
589
|
-
row = {
|
|
590
|
-
ref,
|
|
591
|
-
displayName: input.displayName,
|
|
592
|
-
metadata: input.metadata,
|
|
593
|
-
createdAt: w.now(),
|
|
594
|
-
version: 0,
|
|
595
|
-
state: null,
|
|
596
|
-
history: [],
|
|
597
|
-
profile: null
|
|
598
|
-
};
|
|
599
|
-
computeState(w, row, w.now());
|
|
600
|
-
w.accounts.set(ref, row);
|
|
601
|
-
} else {
|
|
602
|
-
row.displayName = input.displayName;
|
|
603
|
-
row.metadata = input.metadata;
|
|
604
|
-
}
|
|
605
|
-
const { state } = commitAccountChange(w, ref, "account.upserted");
|
|
606
|
-
return {
|
|
607
|
-
account: {
|
|
608
|
-
...toAccount(row),
|
|
609
|
-
account: state
|
|
610
|
-
},
|
|
611
|
-
created
|
|
612
|
-
};
|
|
613
|
-
}
|
|
614
|
-
/** `saveBillingProfile`: bireyselde kurumsal alanlar düşer. */
|
|
615
|
-
function saveProfile(row, profile) {
|
|
616
|
-
const company = profile.kind === "company";
|
|
617
|
-
const saved = {
|
|
618
|
-
kind: profile.kind,
|
|
619
|
-
contactName: profile.contactName,
|
|
620
|
-
...company && profile.companyName !== void 0 ? { companyName: profile.companyName } : {},
|
|
621
|
-
...company && profile.taxOffice !== void 0 ? { taxOffice: profile.taxOffice } : {},
|
|
622
|
-
...company && profile.taxId !== void 0 ? { taxId: profile.taxId } : {},
|
|
623
|
-
email: profile.email,
|
|
624
|
-
phone: profile.phone,
|
|
625
|
-
address: profile.address,
|
|
626
|
-
city: profile.city,
|
|
627
|
-
country: profile.country
|
|
628
|
-
};
|
|
629
|
-
row.profile = saved;
|
|
630
|
-
return saved;
|
|
631
|
-
}
|
|
632
|
-
function accountDetail(w, ref, include) {
|
|
633
|
-
const row = w.accounts.get(ref);
|
|
634
|
-
if (!row) return null;
|
|
635
|
-
const detail = toAccount(row);
|
|
636
|
-
const want = new Set(include);
|
|
637
|
-
const newest = (rows) => [...rows].sort((a, b) => b.createdAt - a.createdAt || (a.id < b.id ? 1 : -1));
|
|
638
|
-
const subs = [...w.subscriptions.filter((s) => s.accountRef === ref)].sort((a, b) => b.startedAt - a.startedAt);
|
|
639
|
-
if (want.has("profile")) detail.profile = row.profile;
|
|
640
|
-
if (want.has("subscriptions")) detail.subscriptions = subs.map((s) => toSubscription(w, s));
|
|
641
|
-
if (want.has("payments")) {
|
|
642
|
-
const ids = new Set(subs.map((s) => s.id));
|
|
643
|
-
detail.payments = [...w.payments.filter((p) => ids.has(p.subscriptionId))].sort((a, b) => b.occurredAt - a.occurredAt).map((p) => ({
|
|
644
|
-
id: p.id,
|
|
645
|
-
subscriptionId: p.subscriptionId,
|
|
646
|
-
providerOrderRef: p.providerOrderRef,
|
|
647
|
-
status: p.status,
|
|
648
|
-
amountMinor: p.amountMinor,
|
|
649
|
-
currency: p.currency,
|
|
650
|
-
periodStart: isoOrNull(p.periodStart),
|
|
651
|
-
periodEnd: isoOrNull(p.periodEnd),
|
|
652
|
-
occurredAt: iso(p.occurredAt)
|
|
653
|
-
}));
|
|
654
|
-
}
|
|
655
|
-
if (want.has("checkouts")) detail.checkouts = newest(w.sessions.filter((s) => s.accountRef === ref)).slice(0, 20).map((s) => toCheckoutSession(w, s));
|
|
656
|
-
if (want.has("grants")) detail.grants = newest(w.grants.filter((g) => g.accountRef === ref)).map(toGrant);
|
|
657
|
-
if (want.has("invoices")) detail.invoices = newest(w.invoices.filter((i) => i.accountRef === ref)).map(toInvoice);
|
|
658
|
-
return detail;
|
|
659
|
-
}
|
|
660
|
-
/** Hosted checkout sayfası (servisin `hostedCheckoutUrl`); sahtede `hosted.ts` sunar. */
|
|
661
|
-
const hostedPageUrl = (baseUrl, id, pageKey) => `${baseUrl}/public/v1/checkout/${id}?k=${pageKey}`;
|
|
662
|
-
const PAGE_KEY_BYTES = 32;
|
|
663
|
-
/** Servis: `appUrl`'in origin'i + `extraReturnOrigins` (sahte, ayarlanmadıysa her adresi kabul eder). */
|
|
664
|
-
function returnUrlAllowed(w, url) {
|
|
665
|
-
const { settings } = w;
|
|
666
|
-
return !(!w.permissiveReturnOrigins || settings.appUrl !== void 0 || settings.extraReturnOrigins.length > 0) || isReturnUrlAllowed(settings, url);
|
|
667
|
-
}
|
|
668
|
-
/** Planın dönem + para biriminde aktif, satılabilir fiyatı (servis: `intervalCount = 1`). */
|
|
669
|
-
function sellablePrice(w, planCode, interval, currency) {
|
|
670
|
-
return w.prices.find((p) => p.active && p.interval === interval && p.intervalCount === 1 && p.currency === currency && p.planCode === planCode && w.plans.get(p.planCode)?.active === true && w.plans.get(p.planCode)?.sellable === true);
|
|
671
|
-
}
|
|
672
|
-
function randomPageKey() {
|
|
673
|
-
const bytes = globalThis.crypto.getRandomValues(new Uint8Array(PAGE_KEY_BYTES));
|
|
674
|
-
return btoa(String.fromCharCode(...bytes)).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
|
|
675
|
-
}
|
|
676
|
-
/**
|
|
677
|
-
* `createHostedCheckoutSession` (service): no provider call; validation order and codes match the
|
|
678
|
-
* service (account, return origins, live subscription, sellable price). A missing account is
|
|
679
|
-
* opened with `displayName`. `url` is the fake steward's hosted page (`hosted.ts`).
|
|
680
|
-
*/
|
|
681
|
-
function createCheckoutSession(w, baseUrl, ref, input, actorRef) {
|
|
682
|
-
const existing = w.accounts.get(ref);
|
|
683
|
-
if (!existing && input.displayName === void 0) return fail("account_not_found");
|
|
684
|
-
const { settings } = w;
|
|
685
|
-
if (!returnUrlAllowed(w, input.successUrl)) return fail("return_url_not_allowed");
|
|
686
|
-
if (input.cancelUrl !== void 0 && !returnUrlAllowed(w, input.cancelUrl)) return fail("return_url_not_allowed");
|
|
687
|
-
if (existing && liveSubscription(w, ref)) return fail("already_subscribed");
|
|
688
|
-
const price = sellablePrice(w, input.planCode, input.interval, input.currency ?? settings.defaultCurrency);
|
|
689
|
-
if (!price) return fail("plan_not_sellable");
|
|
690
|
-
if (!existing) upsertAccount(w, ref, {
|
|
691
|
-
displayName: input.displayName,
|
|
692
|
-
metadata: {}
|
|
693
|
-
});
|
|
694
|
-
const now = w.now();
|
|
695
|
-
const pageKey = randomPageKey();
|
|
696
|
-
const session = {
|
|
697
|
-
id: w.id(),
|
|
698
|
-
accountRef: ref,
|
|
699
|
-
priceId: price.id,
|
|
700
|
-
status: "open",
|
|
701
|
-
locale: input.locale ?? settings.defaultLocale,
|
|
702
|
-
successUrl: input.successUrl,
|
|
703
|
-
cancelUrl: input.cancelUrl ?? null,
|
|
704
|
-
consentDocs: (input.consents ?? []).map(({ document, version, url }) => ({
|
|
705
|
-
document,
|
|
706
|
-
version,
|
|
707
|
-
url
|
|
708
|
-
})),
|
|
709
|
-
prefill: input.prefill ?? null,
|
|
710
|
-
pageKey,
|
|
711
|
-
consents: [],
|
|
712
|
-
actorRef,
|
|
713
|
-
failureReason: null,
|
|
714
|
-
subscriptionId: null,
|
|
715
|
-
expiresAt: now + settings.checkoutTtlMinutes * 6e4,
|
|
716
|
-
createdAt: now,
|
|
717
|
-
completedAt: null
|
|
718
|
-
};
|
|
719
|
-
w.sessions.push(session);
|
|
720
|
-
const { state } = commitAccountChange(w, ref, "checkout.opened");
|
|
721
|
-
return ok({
|
|
722
|
-
...toCheckoutSession(w, session),
|
|
723
|
-
url: hostedPageUrl(baseUrl, session.id, pageKey),
|
|
724
|
-
account: state
|
|
725
|
-
});
|
|
726
|
-
}
|
|
727
|
-
function checkoutSessionResponse(w, id) {
|
|
728
|
-
const row = w.sessions.find((s) => s.id === id);
|
|
729
|
-
if (!row) return null;
|
|
730
|
-
const session = toCheckoutSession(w, row);
|
|
731
|
-
return row.status === "completed" ? {
|
|
732
|
-
...session,
|
|
733
|
-
account: storedState(w, row.accountRef)
|
|
734
|
-
} : session;
|
|
735
|
-
}
|
|
736
|
-
function sessionRow(w, id) {
|
|
737
|
-
const row = w.sessions.find((s) => s.id === id);
|
|
738
|
-
if (!row) throw new Error(`sahte steward: checkout oturumu yok ${id}`);
|
|
739
|
-
return row;
|
|
740
|
-
}
|
|
741
|
-
/** Kapanan oturumda sayfa anahtarı ve ön doldurma silinir (servis `CLOSED_SESSION_CLEANUP`). */
|
|
742
|
-
const CLOSED_SESSION_CLEANUP = {
|
|
743
|
-
pageKey: null,
|
|
744
|
-
prefill: null
|
|
745
|
-
};
|
|
746
|
-
/** Hosted oturumun sayfası açık mı: anahtar bu oturumun, oturum açık ve süresi geçmemiş (servis `openSession`). */
|
|
747
|
-
function openHostedSession(w, id, pageKey) {
|
|
748
|
-
const row = w.sessions.find((s) => s.id === id);
|
|
749
|
-
if (!row || row.pageKey === null || pageKey.length === 0 || row.pageKey !== pageKey) return null;
|
|
750
|
-
return row.status === "open" && row.expiresAt > w.now() ? row : null;
|
|
751
|
-
}
|
|
752
|
-
/** Sahte `details` gönderiminin profil değerleri (ürünün `prefill`i; geri kalanı sabit, geçerli test verisi). */
|
|
753
|
-
function hostedProfile(row, prefill) {
|
|
754
|
-
const company = prefill?.kind === "company";
|
|
755
|
-
return {
|
|
756
|
-
kind: company ? "company" : "individual",
|
|
757
|
-
contactName: prefill?.name ?? row.displayName,
|
|
758
|
-
...company ? {
|
|
759
|
-
companyName: row.displayName,
|
|
760
|
-
taxOffice: "Sahte Vergi Dairesi",
|
|
761
|
-
taxId: "1234567890"
|
|
762
|
-
} : {},
|
|
763
|
-
email: prefill?.email ?? "musteri@example.com",
|
|
764
|
-
phone: "+905550000000",
|
|
765
|
-
address: "Sahte Mah. Örnek Sok. No: 1",
|
|
766
|
-
city: "İstanbul",
|
|
767
|
-
country: "TR"
|
|
768
|
-
};
|
|
769
|
-
}
|
|
770
|
-
/**
|
|
771
|
-
* Hosted oturum (F2b): sahte sayfada `details` adımı yok. Ödeme sonucundan önce servisteki
|
|
772
|
-
* gönderimin izi yazılır: hesabın profili yoksa ön doldurmadan sahte profil (varsa aynen
|
|
773
|
-
* kalır: sayfa saklı profili ön doldurur), belgelerin onay izi, `billing_profile.updated`.
|
|
774
|
-
* Hesap bu arada abone olduysa servis `details`'te oturumu kapatır;
|
|
775
|
-
* çağıranlar o durumda bunu çağırmaz.
|
|
776
|
-
*/
|
|
777
|
-
function submitHostedDetails(w, session) {
|
|
778
|
-
const row = account(w, session.accountRef);
|
|
779
|
-
if (!row.profile) saveProfile(row, hostedProfile(row, session.prefill));
|
|
780
|
-
const acceptedAt = iso(w.now());
|
|
781
|
-
session.consents = session.consentDocs.map(({ document, version, url }) => ({
|
|
782
|
-
document,
|
|
783
|
-
version,
|
|
784
|
-
url,
|
|
785
|
-
acceptedAt
|
|
786
|
-
}));
|
|
787
|
-
commitAccountChange(w, session.accountRef, "billing_profile.updated");
|
|
788
|
-
}
|
|
789
|
-
/** `completeCheckoutReturn` başarısızlık yolu: yalnızca `open` oturum kapanır. */
|
|
790
|
-
function failCheckout(w, session, reason) {
|
|
791
|
-
if (session.status !== "open") return;
|
|
792
|
-
if (!liveSubscription(w, session.accountRef)) submitHostedDetails(w, session);
|
|
793
|
-
Object.assign(session, {
|
|
794
|
-
status: "failed",
|
|
795
|
-
failureReason: reason,
|
|
796
|
-
completedAt: w.now(),
|
|
797
|
-
...CLOSED_SESSION_CLEANUP
|
|
798
|
-
});
|
|
799
|
-
commitAccountChange(w, session.accountRef, {
|
|
800
|
-
type: "checkout.failed",
|
|
801
|
-
detail: {
|
|
802
|
-
checkoutSessionId: session.id,
|
|
803
|
-
reason
|
|
804
|
-
}
|
|
805
|
-
});
|
|
806
|
-
}
|
|
807
|
-
/**
|
|
808
|
-
* Hosted sayfada vazgeç (`HostedCheckoutPort.cancel`; sahte sayfada sağlayıcı formu olmadığından
|
|
809
|
-
* her zaman kapanır): yalnızca açık oturum; `canceled`, iç neden `checkout.canceled`. Dönüş
|
|
810
|
-
* adresi `cancelUrl ?? settings.appUrl` (yoksa null). Açık olmayan oturumda değişiklik yok.
|
|
811
|
-
*/
|
|
812
|
-
function cancelCheckout(w, session) {
|
|
813
|
-
const redirectUrl = session.cancelUrl ?? w.settings.appUrl ?? null;
|
|
814
|
-
if (session.status !== "open") return redirectUrl;
|
|
815
|
-
Object.assign(session, {
|
|
816
|
-
status: "canceled",
|
|
817
|
-
completedAt: w.now(),
|
|
818
|
-
...CLOSED_SESSION_CLEANUP
|
|
819
|
-
});
|
|
820
|
-
commitAccountChange(w, session.accountRef, "checkout.canceled");
|
|
821
|
-
return redirectUrl;
|
|
822
|
-
}
|
|
823
|
-
/** `completeCheckoutReturn` başarı yolu: abonelik, ilk tahsilat, event'ler, taslak fatura. */
|
|
824
|
-
function completeCheckout(w, session) {
|
|
825
|
-
if (session.status !== "open") return;
|
|
826
|
-
const alreadySubscribed = liveSubscription(w, session.accountRef) !== null;
|
|
827
|
-
if (!alreadySubscribed) submitHostedDetails(w, session);
|
|
828
|
-
const now = w.now();
|
|
829
|
-
const price = priceOf(w, session.priceId);
|
|
830
|
-
Object.assign(session, {
|
|
831
|
-
status: "completed",
|
|
832
|
-
completedAt: now,
|
|
833
|
-
...CLOSED_SESSION_CLEANUP
|
|
834
|
-
});
|
|
835
|
-
if (alreadySubscribed) {
|
|
836
|
-
Object.assign(session, {
|
|
837
|
-
status: "failed",
|
|
838
|
-
failureReason: "already_subscribed"
|
|
839
|
-
});
|
|
840
|
-
commitAccountChange(w, session.accountRef, {
|
|
841
|
-
type: "checkout.failed",
|
|
842
|
-
detail: {
|
|
843
|
-
checkoutSessionId: session.id,
|
|
844
|
-
reason: "already_subscribed"
|
|
845
|
-
}
|
|
846
|
-
});
|
|
847
|
-
return;
|
|
848
|
-
}
|
|
849
|
-
const periodEnd = addInterval(now, price.interval, price.intervalCount);
|
|
850
|
-
const sub = {
|
|
851
|
-
id: w.id(),
|
|
852
|
-
accountRef: session.accountRef,
|
|
853
|
-
priceId: price.id,
|
|
854
|
-
providerSubscriptionRef: `fake_sub_${globalThis.crypto.randomUUID()}`,
|
|
855
|
-
status: "active",
|
|
856
|
-
cancelAtPeriodEnd: false,
|
|
857
|
-
cancelAt: null,
|
|
858
|
-
cancelReason: null,
|
|
859
|
-
canceledAt: null,
|
|
860
|
-
startedAt: now,
|
|
861
|
-
currentPeriodStart: now,
|
|
862
|
-
currentPeriodEnd: periodEnd,
|
|
863
|
-
dunningStartedAt: null,
|
|
864
|
-
pastDueSince: null,
|
|
865
|
-
dunningStep: 0,
|
|
866
|
-
nextDunningAt: null,
|
|
867
|
-
lastPaymentRetryAt: null
|
|
868
|
-
};
|
|
869
|
-
w.subscriptions.push(sub);
|
|
870
|
-
session.subscriptionId = sub.id;
|
|
871
|
-
const payment = insertPayment(w, sub, {
|
|
872
|
-
providerOrderRef: `checkout:${session.id}`,
|
|
873
|
-
status: "succeeded",
|
|
874
|
-
periodStart: now,
|
|
875
|
-
periodEnd
|
|
876
|
-
});
|
|
877
|
-
commitAccountChange(w, session.accountRef, {
|
|
878
|
-
type: "subscription.activated",
|
|
879
|
-
detail: {
|
|
880
|
-
subscriptionId: sub.id,
|
|
881
|
-
planCode: price.planCode,
|
|
882
|
-
interval: price.interval,
|
|
883
|
-
amountMinor: price.amountMinor,
|
|
884
|
-
currency: price.currency,
|
|
885
|
-
actorRef: session.actorRef
|
|
886
|
-
}
|
|
887
|
-
});
|
|
888
|
-
commitAccountChange(w, session.accountRef, {
|
|
889
|
-
type: "checkout.completed",
|
|
890
|
-
detail: {
|
|
891
|
-
checkoutSessionId: session.id,
|
|
892
|
-
subscriptionId: sub.id
|
|
893
|
-
}
|
|
894
|
-
});
|
|
895
|
-
createInvoiceForPayment(w, payment);
|
|
896
|
-
}
|
|
897
|
-
function insertPayment(w, sub, fields) {
|
|
898
|
-
const price = priceOf(w, sub.priceId);
|
|
899
|
-
const now = w.now();
|
|
900
|
-
const payment = {
|
|
901
|
-
id: w.id(),
|
|
902
|
-
subscriptionId: sub.id,
|
|
903
|
-
attempt: 1,
|
|
904
|
-
amountMinor: price.amountMinor,
|
|
905
|
-
currency: price.currency,
|
|
906
|
-
occurredAt: now,
|
|
907
|
-
createdAt: now,
|
|
908
|
-
...fields
|
|
909
|
-
};
|
|
910
|
-
w.payments.push(payment);
|
|
911
|
-
return payment;
|
|
912
|
-
}
|
|
913
|
-
const day$1 = (ms) => ms === null ? "?" : iso(ms).slice(0, 10);
|
|
914
|
-
/** Başarılı tahsilat için taslak fatura (idempotent); profil yoksa fatura yok. */
|
|
915
|
-
function createInvoiceForPayment(w, payment) {
|
|
916
|
-
const existing = w.invoices.find((i) => i.paymentId === payment.id);
|
|
917
|
-
if (existing) return existing.id;
|
|
918
|
-
if (payment.status !== "succeeded") return null;
|
|
919
|
-
const sub = w.subscriptions.find((s) => s.id === payment.subscriptionId);
|
|
920
|
-
const price = priceOf(w, sub.priceId);
|
|
921
|
-
const profile = w.accounts.get(sub.accountRef)?.profile;
|
|
922
|
-
if (!profile) return null;
|
|
923
|
-
const totals = price.taxInclusive ? {
|
|
924
|
-
...splitInclusiveTax(payment.amountMinor, price.taxRateBps),
|
|
925
|
-
totalMinor: payment.amountMinor
|
|
926
|
-
} : (() => {
|
|
927
|
-
const taxMinor = Math.round(payment.amountMinor * price.taxRateBps / 1e4);
|
|
928
|
-
return {
|
|
929
|
-
subtotalMinor: payment.amountMinor,
|
|
930
|
-
taxMinor,
|
|
931
|
-
totalMinor: payment.amountMinor + taxMinor
|
|
932
|
-
};
|
|
933
|
-
})();
|
|
934
|
-
const invoice = {
|
|
935
|
-
id: w.id(),
|
|
936
|
-
accountRef: sub.accountRef,
|
|
937
|
-
subscriptionId: sub.id,
|
|
938
|
-
paymentId: payment.id,
|
|
939
|
-
status: "draft",
|
|
940
|
-
number: null,
|
|
941
|
-
currency: payment.currency,
|
|
942
|
-
...totals,
|
|
943
|
-
taxRateBps: price.taxRateBps,
|
|
944
|
-
buyer: profile,
|
|
945
|
-
lines: [{
|
|
946
|
-
description: `${w.plans.get(price.planCode).name} (${day$1(payment.periodStart)} – ${day$1(payment.periodEnd)})`,
|
|
947
|
-
quantity: 1,
|
|
948
|
-
unitAmountMinor: totals.subtotalMinor,
|
|
949
|
-
amountMinor: totals.subtotalMinor
|
|
950
|
-
}],
|
|
951
|
-
externalRef: null,
|
|
952
|
-
issuedAt: null,
|
|
953
|
-
createdAt: w.now(),
|
|
954
|
-
document: null
|
|
955
|
-
};
|
|
956
|
-
w.invoices.push(invoice);
|
|
957
|
-
commitAccountChange(w, sub.accountRef, {
|
|
958
|
-
type: "invoice.created",
|
|
959
|
-
detail: {
|
|
960
|
-
invoiceId: invoice.id,
|
|
961
|
-
totalMinor: totals.totalMinor,
|
|
962
|
-
currency: payment.currency
|
|
963
|
-
}
|
|
964
|
-
});
|
|
965
|
-
return invoice.id;
|
|
966
|
-
}
|
|
967
|
-
const MAX_DOCUMENT_BYTES = 5242880;
|
|
968
|
-
function issueInvoice(w, id, input) {
|
|
969
|
-
let document = null;
|
|
970
|
-
if (input.documentBase64 !== void 0) {
|
|
971
|
-
document = Uint8Array.from(atob(input.documentBase64), (c) => c.charCodeAt(0));
|
|
972
|
-
const signature = String.fromCharCode(...document.subarray(0, 5));
|
|
973
|
-
if (document.length === 0 || document.length > MAX_DOCUMENT_BYTES || signature !== "%PDF-") return fail("invalid_document");
|
|
974
|
-
}
|
|
975
|
-
const invoice = w.invoices.find((i) => i.id === id);
|
|
976
|
-
if (!invoice) return fail("invoice_not_found");
|
|
977
|
-
if (invoice.status !== "draft") return fail("already_issued");
|
|
978
|
-
if (w.invoices.some((i) => i.number === input.number)) return fail("number_taken");
|
|
979
|
-
Object.assign(invoice, {
|
|
980
|
-
status: "issued",
|
|
981
|
-
number: input.number,
|
|
982
|
-
issuedAt: input.issuedAt !== void 0 ? Date.parse(input.issuedAt) : w.now(),
|
|
983
|
-
externalRef: input.externalRef ?? null,
|
|
984
|
-
document: document && {
|
|
985
|
-
bytes: document,
|
|
986
|
-
sha256: sha256Hex(document)
|
|
987
|
-
}
|
|
988
|
-
});
|
|
989
|
-
const { state } = commitAccountChange(w, invoice.accountRef, {
|
|
990
|
-
type: "invoice.issued",
|
|
991
|
-
detail: {
|
|
992
|
-
invoiceId: id,
|
|
993
|
-
number: input.number,
|
|
994
|
-
totalMinor: invoice.totalMinor,
|
|
995
|
-
currency: invoice.currency
|
|
996
|
-
}
|
|
997
|
-
});
|
|
998
|
-
return ok({
|
|
999
|
-
...toInvoice(invoice),
|
|
1000
|
-
account: state
|
|
1001
|
-
});
|
|
1002
|
-
}
|
|
1003
|
-
function cancelSubscription(w, id, input, actorRef) {
|
|
1004
|
-
const sub = w.subscriptions.find((s) => s.id === id);
|
|
1005
|
-
if (!sub) return fail("subscription_not_found");
|
|
1006
|
-
if (!isLive(sub)) return fail("already_canceled");
|
|
1007
|
-
if (input.mode === "at_period_end" && sub.cancelAtPeriodEnd) return ok({
|
|
1008
|
-
...toSubscription(w, sub),
|
|
1009
|
-
account: storedState(w, sub.accountRef)
|
|
1010
|
-
});
|
|
1011
|
-
const now = w.now();
|
|
1012
|
-
if (input.mode === "immediate") Object.assign(sub, {
|
|
1013
|
-
status: "canceled",
|
|
1014
|
-
cancelAt: now,
|
|
1015
|
-
canceledAt: now,
|
|
1016
|
-
pastDueSince: null,
|
|
1017
|
-
dunningStartedAt: null,
|
|
1018
|
-
dunningStep: 0,
|
|
1019
|
-
nextDunningAt: null
|
|
1020
|
-
});
|
|
1021
|
-
else Object.assign(sub, {
|
|
1022
|
-
cancelAtPeriodEnd: true,
|
|
1023
|
-
cancelAt: sub.currentPeriodEnd !== null && sub.currentPeriodEnd > now ? sub.currentPeriodEnd : now,
|
|
1024
|
-
canceledAt: now
|
|
1025
|
-
});
|
|
1026
|
-
sub.cancelReason = input.reason;
|
|
1027
|
-
const { state } = commitAccountChange(w, sub.accountRef, input.mode === "immediate" ? {
|
|
1028
|
-
type: "subscription.canceled",
|
|
1029
|
-
detail: {
|
|
1030
|
-
subscriptionId: sub.id,
|
|
1031
|
-
reason: input.reason,
|
|
1032
|
-
effectiveAt: iso(now),
|
|
1033
|
-
actorRef
|
|
1034
|
-
}
|
|
1035
|
-
} : {
|
|
1036
|
-
type: "subscription.cancel_scheduled",
|
|
1037
|
-
detail: {
|
|
1038
|
-
subscriptionId: sub.id,
|
|
1039
|
-
cancelAt: iso(sub.cancelAt),
|
|
1040
|
-
reason: input.reason,
|
|
1041
|
-
actorRef
|
|
1042
|
-
}
|
|
1043
|
-
});
|
|
1044
|
-
return ok({
|
|
1045
|
-
...toSubscription(w, sub),
|
|
1046
|
-
account: state
|
|
1047
|
-
});
|
|
1048
|
-
}
|
|
1049
|
-
function createGrant(w, ref, input, actorRef) {
|
|
1050
|
-
if (!w.accounts.has(ref)) return fail("account_not_found");
|
|
1051
|
-
if (input.planCode !== void 0 && w.plans.get(input.planCode)?.active !== true) return fail("plan_not_found");
|
|
1052
|
-
const mismatched = overrideTypeMismatches(w, input.overrides);
|
|
1053
|
-
if (mismatched !== null) return fail("override_type_mismatch", mismatched);
|
|
1054
|
-
const now = w.now();
|
|
1055
|
-
const startsAt = input.startsAt !== void 0 ? Date.parse(input.startsAt) : null;
|
|
1056
|
-
const endsAt = input.endsAt !== void 0 ? Date.parse(input.endsAt) : null;
|
|
1057
|
-
if (endsAt !== null && endsAt <= (startsAt ?? now)) return fail("invalid_window");
|
|
1058
|
-
const grant = {
|
|
1059
|
-
id: w.id(),
|
|
1060
|
-
accountRef: ref,
|
|
1061
|
-
planCode: input.planCode ?? null,
|
|
1062
|
-
overrides: input.overrides,
|
|
1063
|
-
reason: input.reason,
|
|
1064
|
-
actorRef,
|
|
1065
|
-
startsAt: startsAt ?? now,
|
|
1066
|
-
endsAt,
|
|
1067
|
-
revokedAt: null,
|
|
1068
|
-
createdAt: now,
|
|
1069
|
-
startApplied: false,
|
|
1070
|
-
endApplied: false
|
|
1071
|
-
};
|
|
1072
|
-
w.grants.push(grant);
|
|
1073
|
-
const { state } = commitAccountChange(w, ref, "grant.created");
|
|
1074
|
-
return ok({
|
|
1075
|
-
...toGrant(grant),
|
|
1076
|
-
account: state
|
|
1077
|
-
});
|
|
1078
|
-
}
|
|
1079
|
-
/** Zaten iptal edilmiş grant: idempotent başarı. */
|
|
1080
|
-
function revokeGrant(w, id) {
|
|
1081
|
-
const grant = w.grants.find((g) => g.id === id);
|
|
1082
|
-
if (!grant) return false;
|
|
1083
|
-
if (grant.revokedAt !== null) return true;
|
|
1084
|
-
grant.revokedAt = w.now();
|
|
1085
|
-
commitAccountChange(w, grant.accountRef, "grant.revoked");
|
|
1086
|
-
return true;
|
|
1087
|
-
}
|
|
1088
|
-
/** `overrideTypeMismatches`: katalog feature tipine uymayan override'lar, uyumluysa null. */
|
|
1089
|
-
function overrideTypeMismatches(w, overrides) {
|
|
1090
|
-
const types = new Map(w.features.map((f) => [f.key, f.type]));
|
|
1091
|
-
const mismatched = Object.entries(overrides).filter(([key, value]) => types.has(key) && !matchesType(types.get(key), value)).map(([key]) => `${key}: ${types.get(key)} bekleniyor`).sort();
|
|
1092
|
-
return mismatched.length > 0 ? mismatched.join(", ") : null;
|
|
1093
|
-
}
|
|
1094
|
-
/**
|
|
1095
|
-
* `setPlan` (K6): aktif plan grant'leri (henüz başlamamışlar dahil; override
|
|
1096
|
-
* grant'leri hariç) iptal + yeni plan grant'i + `commitAccountChange(grant.plan_set)`.
|
|
1097
|
-
* Tek aktif plan grant'i aynı plan/`endsAt`/`overrides` ise korunur. Canlı abonelik
|
|
1098
|
-
* planı hak kuralında hedefi geçiyorsa `strict` → `subscription_plan_higher` (yazım
|
|
1099
|
-
* yok), `ensure` → uyarı. Sahtede eşzamanlılık yok (servis advisory lock'la sıralar).
|
|
1100
|
-
*/
|
|
1101
|
-
function setPlan(w, ref, input, actorRef) {
|
|
1102
|
-
if (!w.accounts.has(ref)) return fail("account_not_found");
|
|
1103
|
-
const { planCode } = input;
|
|
1104
|
-
if (planCode !== null && w.plans.get(planCode)?.active !== true) return fail("plan_not_found");
|
|
1105
|
-
const mismatched = overrideTypeMismatches(w, input.overrides);
|
|
1106
|
-
if (mismatched !== null) return fail("override_type_mismatch", mismatched);
|
|
1107
|
-
const now = w.now();
|
|
1108
|
-
const endsAt = input.endsAt !== void 0 ? Date.parse(input.endsAt) : null;
|
|
1109
|
-
if (endsAt !== null && endsAt <= now) return fail("invalid_window");
|
|
1110
|
-
const warnings = [];
|
|
1111
|
-
const live = liveSubscription(w, ref);
|
|
1112
|
-
if (live && w.defaultPlanCode !== null) {
|
|
1113
|
-
const livePlanCode = priceOf(w, live.priceId).planCode;
|
|
1114
|
-
const intended = planCode ?? w.defaultPlanCode;
|
|
1115
|
-
const { planCode: effective } = resolveEntitlements({
|
|
1116
|
-
catalog: {
|
|
1117
|
-
defaultPlanCode: w.defaultPlanCode,
|
|
1118
|
-
features: [],
|
|
1119
|
-
plans: [...w.plans.values()].map((p) => ({
|
|
1120
|
-
code: p.code,
|
|
1121
|
-
rank: p.rank,
|
|
1122
|
-
entitlements: {}
|
|
1123
|
-
}))
|
|
1124
|
-
},
|
|
1125
|
-
subscription: {
|
|
1126
|
-
planCode: livePlanCode,
|
|
1127
|
-
status: live.status
|
|
1128
|
-
},
|
|
1129
|
-
grants: planCode !== null ? [{ planCode }] : []
|
|
1130
|
-
});
|
|
1131
|
-
if (effective !== intended && effective === livePlanCode) {
|
|
1132
|
-
const message = `canlı abonelik planı (${livePlanCode}) hedeften (${intended}) yüksek; etkin plan ${livePlanCode}`;
|
|
1133
|
-
if (input.mode === "strict") return fail("subscription_plan_higher", `${message}; hiçbir şey yazılmadı`);
|
|
1134
|
-
warnings.push({
|
|
1135
|
-
code: "subscription_plan_higher",
|
|
1136
|
-
message
|
|
1137
|
-
});
|
|
1138
|
-
}
|
|
1139
|
-
}
|
|
1140
|
-
const active = w.grants.filter((g) => g.accountRef === ref && g.planCode !== null && g.revokedAt === null && (g.endsAt === null || g.endsAt > now)).sort((a, b) => a.createdAt - b.createdAt || (a.id < b.id ? -1 : 1));
|
|
1141
|
-
const [only] = active;
|
|
1142
|
-
const same = planCode !== null && active.length === 1 && only !== void 0 && only.startsAt <= now && only.planCode === planCode && only.endsAt === endsAt && canonicalJson(only.overrides) === canonicalJson(input.overrides);
|
|
1143
|
-
let grant = same ? only : null;
|
|
1144
|
-
if (!same) {
|
|
1145
|
-
for (const g of active) g.revokedAt = now;
|
|
1146
|
-
if (planCode !== null) {
|
|
1147
|
-
grant = {
|
|
1148
|
-
id: w.id(),
|
|
1149
|
-
accountRef: ref,
|
|
1150
|
-
planCode,
|
|
1151
|
-
overrides: input.overrides,
|
|
1152
|
-
reason: input.reason,
|
|
1153
|
-
actorRef,
|
|
1154
|
-
startsAt: now,
|
|
1155
|
-
endsAt,
|
|
1156
|
-
revokedAt: null,
|
|
1157
|
-
createdAt: now,
|
|
1158
|
-
startApplied: false,
|
|
1159
|
-
endApplied: false
|
|
1160
|
-
};
|
|
1161
|
-
w.grants.push(grant);
|
|
1162
|
-
}
|
|
1163
|
-
}
|
|
1164
|
-
const { state } = commitAccountChange(w, ref, "grant.plan_set");
|
|
1165
|
-
return ok({
|
|
1166
|
-
grant: grant ? toGrant(grant) : null,
|
|
1167
|
-
account: state,
|
|
1168
|
-
warnings
|
|
1169
|
-
});
|
|
1170
|
-
}
|
|
1171
|
-
/** `resyncAccount`: saklı durum `account.state_changed` (`cause: "resync"`) olarak; version aynı. */
|
|
1172
|
-
function resyncAccount(w, ref) {
|
|
1173
|
-
if (!w.accounts.has(ref)) return fail("account_not_found");
|
|
1174
|
-
return ok({
|
|
1175
|
-
queued: true,
|
|
1176
|
-
eventId: republishState(w, ref, storedState(w, ref))
|
|
1177
|
-
});
|
|
1178
|
-
}
|
|
1179
|
-
/**
|
|
1180
|
-
* `deleteAccount` (`domain/account-deletion.ts`, F4e): önkoşullar sırayla (hesap yok 404,
|
|
1181
|
-
* silinmiş 410, canlı abonelik) ve hiçbiri tutmazsa değişiklik yok. The service's
|
|
1182
|
-
* `checkout_in_progress` (open provider form) never happens here: the fake hosted page has no
|
|
1183
|
-
* provider form, so open sessions are formless and deletion closes them. Sonra anonimleştirme:
|
|
1184
|
-
* ad/metadata/profil, oturumların ön doldurması/anahtarı ve onay izinin `ipHash`/`userAgent`'ı,
|
|
1185
|
-
* portal oturumları, aktif grant'ler; eski event yüklerinde `displayName`. Faturalar kalır.
|
|
1186
|
-
*/
|
|
1187
|
-
function deleteAccount(w, ref, actorRef) {
|
|
1188
|
-
const row = w.accounts.get(ref);
|
|
1189
|
-
if (!row) return fail("account_not_found");
|
|
1190
|
-
if (row.deletedAt !== void 0) return fail("account_deleted");
|
|
1191
|
-
const live = liveSubscription(w, ref);
|
|
1192
|
-
if (live) return fail("subscription_active", `abonelik ${live.id} canlı; önce hemen iptal edin (mode: immediate)`);
|
|
1193
|
-
const now = w.now();
|
|
1194
|
-
for (const session of w.sessions.filter((s) => s.accountRef === ref)) {
|
|
1195
|
-
if (session.status === "open") Object.assign(session, {
|
|
1196
|
-
status: "canceled",
|
|
1197
|
-
failureReason: "account_deleted",
|
|
1198
|
-
completedAt: now
|
|
1199
|
-
});
|
|
1200
|
-
Object.assign(session, CLOSED_SESSION_CLEANUP);
|
|
1201
|
-
session.consents = session.consents.map((consent) => {
|
|
1202
|
-
const { ipHash: _ip, userAgent: _ua, ...kept } = consent;
|
|
1203
|
-
return kept;
|
|
1204
|
-
});
|
|
1205
|
-
}
|
|
1206
|
-
w.portalSessions = w.portalSessions.filter((s) => s.accountRef !== ref);
|
|
1207
|
-
for (const grant of w.grants) if (grant.accountRef === ref && grant.revokedAt === null) grant.revokedAt = now;
|
|
1208
|
-
Object.assign(row, {
|
|
1209
|
-
displayName: "",
|
|
1210
|
-
metadata: {},
|
|
1211
|
-
profile: null,
|
|
1212
|
-
deletedAt: now
|
|
1213
|
-
});
|
|
1214
|
-
for (const event of w.events) {
|
|
1215
|
-
const account = event.accountRef === ref ? event.payload.data.account : null;
|
|
1216
|
-
if (account && typeof account.displayName === "string") account.displayName = "";
|
|
1217
|
-
}
|
|
1218
|
-
const deletedAt = iso(now);
|
|
1219
|
-
const change = commitAccountChange(w, ref, {
|
|
1220
|
-
type: "account.deleted",
|
|
1221
|
-
detail: {
|
|
1222
|
-
deletedAt,
|
|
1223
|
-
actorRef
|
|
1224
|
-
}
|
|
1225
|
-
});
|
|
1226
|
-
if (change.stateEventId === null) emitEvent(w, ref, change.state, {
|
|
1227
|
-
type: "account.state_changed",
|
|
1228
|
-
detail: {
|
|
1229
|
-
cause: "account.deleted",
|
|
1230
|
-
causeEventId: change.eventId
|
|
1231
|
-
}
|
|
1232
|
-
});
|
|
1233
|
-
return ok({
|
|
1234
|
-
ref,
|
|
1235
|
-
deletedAt
|
|
1236
|
-
});
|
|
1237
|
-
}
|
|
1238
|
-
function stats(w) {
|
|
1239
|
-
const statuses = [
|
|
1240
|
-
"active",
|
|
1241
|
-
"past_due",
|
|
1242
|
-
"suspended",
|
|
1243
|
-
"canceled",
|
|
1244
|
-
"expired"
|
|
1245
|
-
];
|
|
1246
|
-
const mrr = {};
|
|
1247
|
-
for (const sub of w.subscriptions) {
|
|
1248
|
-
if (!isLive(sub) || sub.cancelAtPeriodEnd) continue;
|
|
1249
|
-
const price = priceOf(w, sub.priceId);
|
|
1250
|
-
const months = (price.interval === "year" ? 12 : 1) * price.intervalCount;
|
|
1251
|
-
mrr[price.currency] = (mrr[price.currency] ?? 0) + price.amountMinor / months;
|
|
1252
|
-
}
|
|
1253
|
-
return {
|
|
1254
|
-
subscriptions: Object.fromEntries(statuses.map((s) => [s, w.subscriptions.filter((sub) => sub.status === s).length])),
|
|
1255
|
-
mrrMinor: Object.fromEntries(Object.entries(mrr).map(([c, v]) => [c, Math.round(v)])),
|
|
1256
|
-
accounts: w.accounts.size
|
|
1257
|
-
};
|
|
1258
|
-
}
|
|
1259
|
-
/** Portal sayfası (servisin `hostedPortalUrl`); sahtede `portal.ts` sunar. */
|
|
1260
|
-
const hostedPortalUrl = (baseUrl, id, pageKey) => `${baseUrl}/public/v1/portal/${id}?k=${pageKey}`;
|
|
1261
|
-
const MAX_PAGE_KEY_LENGTH = 256;
|
|
1262
|
-
/** Portal aksiyonlarının aktörü (servisin `customerActorRef`). */
|
|
1263
|
-
const customerActorRef = (accountRef) => `customer:${accountRef}`;
|
|
1264
|
-
/**
|
|
1265
|
-
* `createPortalSession`: hesap yoksa `account_not_found` (checkout'taki gibi açılmaz), `returnUrl`
|
|
1266
|
-
* origin'i izinli değilse `return_url_not_allowed`; dil verilmezse `defaultLocale`, süre
|
|
1267
|
-
* `portalTtlMinutes`. Anahtar 32 bayt base64url. Durum değişmez, event yok.
|
|
1268
|
-
*/
|
|
1269
|
-
function createPortalSession(w, baseUrl, ref, input, actorRef) {
|
|
1270
|
-
if (!w.accounts.has(ref)) return fail("account_not_found");
|
|
1271
|
-
if (input.returnUrl !== void 0 && !returnUrlAllowed(w, input.returnUrl)) return fail("return_url_not_allowed");
|
|
1272
|
-
const now = w.now();
|
|
1273
|
-
const session = {
|
|
1274
|
-
id: w.id(),
|
|
1275
|
-
accountRef: ref,
|
|
1276
|
-
pageKey: randomPageKey(),
|
|
1277
|
-
locale: input.locale ?? w.settings.defaultLocale,
|
|
1278
|
-
returnUrl: input.returnUrl ?? null,
|
|
1279
|
-
actorRef,
|
|
1280
|
-
expiresAt: now + w.settings.portalTtlMinutes * 6e4,
|
|
1281
|
-
createdAt: now
|
|
1282
|
-
};
|
|
1283
|
-
w.portalSessions.push(session);
|
|
1284
|
-
return ok({
|
|
1285
|
-
id: session.id,
|
|
1286
|
-
url: hostedPortalUrl(baseUrl, session.id, session.pageKey),
|
|
1287
|
-
expiresAt: iso(session.expiresAt)
|
|
1288
|
-
});
|
|
1289
|
-
}
|
|
1290
|
-
/** Süresi geçmemiş portal oturumu (anahtarsız; `simulate`). Yoksa null. */
|
|
1291
|
-
function livePortalSession(w, id) {
|
|
1292
|
-
const row = w.portalSessions.find((s) => s.id === id);
|
|
1293
|
-
return row && row.expiresAt > w.now() ? row : null;
|
|
1294
|
-
}
|
|
1295
|
-
/** Servisin `validSession`'ı: bilinmeyen id, yanlış anahtar ve süresi geçmiş oturum AYNI sonuç (null). */
|
|
1296
|
-
function openPortalSession(w, id, pageKey) {
|
|
1297
|
-
if (pageKey.length === 0 || pageKey.length > MAX_PAGE_KEY_LENGTH) return null;
|
|
1298
|
-
const row = livePortalSession(w, id);
|
|
1299
|
-
return row && row.pageKey === pageKey ? row : null;
|
|
1300
|
-
}
|
|
1301
|
-
function lastFailedPayment(w, sub) {
|
|
1302
|
-
return [...w.payments.filter((p) => p.subscriptionId === sub.id && p.status === "failed")].sort((a, b) => b.occurredAt - a.occurredAt)[0];
|
|
1303
|
-
}
|
|
1304
|
-
/** Servisin portal `load` görünümü (sayfa için). */
|
|
1305
|
-
function portalView(w, session) {
|
|
1306
|
-
const row = account(w, session.accountRef);
|
|
1307
|
-
const state = storedState(w, session.accountRef);
|
|
1308
|
-
const live = liveSubscription(w, session.accountRef);
|
|
1309
|
-
const price = live ? priceOf(w, live.priceId) : null;
|
|
1310
|
-
return {
|
|
1311
|
-
session,
|
|
1312
|
-
account: row,
|
|
1313
|
-
state,
|
|
1314
|
-
planName: w.plans.get(state.planCode)?.name ?? state.planCode,
|
|
1315
|
-
subscription: live && price ? {
|
|
1316
|
-
row: live,
|
|
1317
|
-
price,
|
|
1318
|
-
planName: w.plans.get(price.planCode)?.name ?? price.planCode,
|
|
1319
|
-
canCancel: !live.cancelAtPeriodEnd,
|
|
1320
|
-
canRetryPayment: inDunning(live) && lastFailedPayment(w, live) !== void 0
|
|
1321
|
-
} : null,
|
|
1322
|
-
invoices: [...w.invoices.filter((i) => i.accountRef === session.accountRef && (i.paymentId !== null || i.status !== "draft"))].sort((a, b) => b.createdAt - a.createdAt || (a.id < b.id ? 1 : -1)).slice(0, 24),
|
|
1323
|
-
returnUrl: session.returnUrl ?? w.settings.appUrl ?? null
|
|
1324
|
-
};
|
|
1325
|
-
}
|
|
1326
|
-
/**
|
|
1327
|
-
* Portal iptali (`HostedPortalPort.cancelAtPeriodEnd`): canlı abonelik dönem sonunda
|
|
1328
|
-
* (`cancelSubscription`, neden `customer_portal`, aktör `customer:<ref>`; `/v1` iptaliyle aynı
|
|
1329
|
-
* kurallar ve event'ler). Zaten planlıysa `already_scheduled` (iz/event yok).
|
|
1330
|
-
*/
|
|
1331
|
-
function portalCancel(w, session) {
|
|
1332
|
-
const sub = liveSubscription(w, session.accountRef);
|
|
1333
|
-
if (!sub) return "no_subscription";
|
|
1334
|
-
if (sub.cancelAtPeriodEnd) return "already_scheduled";
|
|
1335
|
-
return cancelSubscription(w, sub.id, {
|
|
1336
|
-
mode: "at_period_end",
|
|
1337
|
-
reason: "customer_portal",
|
|
1338
|
-
skipProvider: false
|
|
1339
|
-
}, customerActorRef(session.accountRef)).ok ? "ok" : "no_subscription";
|
|
1340
|
-
}
|
|
1341
|
-
/**
|
|
1342
|
-
* Portal "ödemeyi yeniden dene" (`HostedPortalPort.retryPayment`): past_due/suspended abonelikte
|
|
1343
|
-
* son başarısız sipariş sahte sağlayıcıya gönderilmiş sayılır — DURUM DEĞİŞMEZ, event yok;
|
|
1344
|
-
* tahsilatın sonucu servisteki gibi sonradan gelir (`simulate.renewal(ref)` aynı siparişi
|
|
1345
|
-
* başarılı/`fail` sayar). Abonelik başına 10 dakikada bir (portal ya da dunning retry'ı).
|
|
1346
|
-
*/
|
|
1347
|
-
function portalRetryPayment(w, session) {
|
|
1348
|
-
const sub = liveSubscription(w, session.accountRef);
|
|
1349
|
-
if (!sub) return "no_subscription";
|
|
1350
|
-
if (!inDunning(sub)) return "nothing_to_retry";
|
|
1351
|
-
const now = w.now();
|
|
1352
|
-
if (sub.lastPaymentRetryAt !== null && sub.lastPaymentRetryAt > now - 6e5) return "retry_too_soon";
|
|
1353
|
-
if (!lastFailedPayment(w, sub)) return "no_failed_payment";
|
|
1354
|
-
sub.lastPaymentRetryAt = now;
|
|
1355
|
-
return "ok";
|
|
1356
|
-
}
|
|
1357
|
-
const PROFILE_REQUIRED = [
|
|
1358
|
-
"kind",
|
|
1359
|
-
"contactName",
|
|
1360
|
-
"email",
|
|
1361
|
-
"phone",
|
|
1362
|
-
"address",
|
|
1363
|
-
"city"
|
|
1364
|
-
];
|
|
1365
|
-
const PROFILE_COMPANY_REQUIRED = [
|
|
1366
|
-
"companyName",
|
|
1367
|
-
"taxOffice",
|
|
1368
|
-
"taxId"
|
|
1369
|
-
];
|
|
1370
|
-
const PROFILE_TEXT = [
|
|
1371
|
-
"contactName",
|
|
1372
|
-
"address",
|
|
1373
|
-
"city"
|
|
1374
|
-
];
|
|
1375
|
-
const PROFILE_COMPANY_TEXT = ["companyName", "taxOffice"];
|
|
1376
|
-
const isBlank = (value) => value === void 0 || value === null || typeof value === "string" && value.trim() === "";
|
|
1377
|
-
/**
|
|
1378
|
-
* Servisin `validateProfile`'ı (hosted-checkout.ts `checkProfile`): alan anahtarları
|
|
1379
|
-
* `profile.<alan>`, alan başına ilk kod; öncelik required > invalid_kind > invalid_characters >
|
|
1380
|
-
* şema kodları > VKN/E.164. Değer koda girmez.
|
|
1381
|
-
*/
|
|
1382
|
-
function validatePortalProfile(input) {
|
|
1383
|
-
const fieldErrors = {};
|
|
1384
|
-
const set = (field, code) => {
|
|
1385
|
-
fieldErrors[`profile.${field}`] ??= code;
|
|
1386
|
-
};
|
|
1387
|
-
const company = input.kind === "company";
|
|
1388
|
-
for (const field of company ? [...PROFILE_REQUIRED, ...PROFILE_COMPANY_REQUIRED] : PROFILE_REQUIRED) if (isBlank(input[field])) set(field, "required");
|
|
1389
|
-
if (!isBlank(input.kind) && input.kind !== "individual" && !company) set("kind", "invalid_kind");
|
|
1390
|
-
for (const field of company ? [...PROFILE_TEXT, ...PROFILE_COMPANY_TEXT] : PROFILE_TEXT) {
|
|
1391
|
-
const value = input[field];
|
|
1392
|
-
if (typeof value === "string" && /[<>]/.test(value)) set(field, "invalid_characters");
|
|
1393
|
-
}
|
|
1394
|
-
const parsed = BillingProfileSchema.safeParse(input);
|
|
1395
|
-
if (!parsed.success) for (const issue of parsed.error.issues) {
|
|
1396
|
-
const field = typeof issue.path[0] === "string" ? issue.path[0] : "";
|
|
1397
|
-
if (field === "") continue;
|
|
1398
|
-
if (isBlank(input[field])) set(field, "required");
|
|
1399
|
-
else if (field === "kind") set(field, "invalid_kind");
|
|
1400
|
-
else if (field === "email") set(field, "invalid_email");
|
|
1401
|
-
else if (field === "phone") set(field, "invalid_phone");
|
|
1402
|
-
else if (field === "taxId" && company) set(field, "invalid_vkn");
|
|
1403
|
-
else if (issue.code === "too_small") set(field, "too_short");
|
|
1404
|
-
else if (issue.code === "too_big") set(field, "too_long");
|
|
1405
|
-
else set(field, "invalid");
|
|
1406
|
-
}
|
|
1407
|
-
if (company && !isBlank(input.taxId) && !isValidVkn(input.taxId)) set("taxId", "invalid_vkn");
|
|
1408
|
-
if (!isBlank(input.phone) && !isE164(input.phone)) set("phone", "invalid_phone");
|
|
1409
|
-
return parsed.success && Object.keys(fieldErrors).length === 0 ? {
|
|
1410
|
-
ok: true,
|
|
1411
|
-
profile: parsed.data
|
|
1412
|
-
} : {
|
|
1413
|
-
ok: false,
|
|
1414
|
-
fieldErrors
|
|
1415
|
-
};
|
|
1416
|
-
}
|
|
1417
|
-
/**
|
|
1418
|
-
* Portal profil güncellemesi (`HostedPortalPort.updateProfile`): ülke verilmezse saklı değer
|
|
1419
|
-
* korunur (yoksa TR); bireyselde kurumsal alanlar düşer. Durumda yalnızca profilin varlığı ve
|
|
1420
|
-
* türü olduğundan event yalnızca bunlar değişince (`billing_profile.updated`).
|
|
1421
|
-
*/
|
|
1422
|
-
function updatePortalProfile(w, session, input) {
|
|
1423
|
-
const row = account(w, session.accountRef);
|
|
1424
|
-
const raw = { ...input };
|
|
1425
|
-
if (raw.country === void 0 && row.profile) raw.country = row.profile.country;
|
|
1426
|
-
const checked = validatePortalProfile(raw);
|
|
1427
|
-
if (!checked.ok) return checked;
|
|
1428
|
-
saveProfile(row, checked.profile);
|
|
1429
|
-
commitAccountChange(w, session.accountRef, "billing_profile.updated");
|
|
1430
|
-
return { ok: true };
|
|
1431
|
-
}
|
|
1432
|
-
/** Portal belge indirme: yalnızca oturumun hesabının kesilmiş ve belgeli faturası; yoksa null. */
|
|
1433
|
-
function portalInvoiceDocument(w, session, invoiceId) {
|
|
1434
|
-
const invoice = w.invoices.find((i) => i.id === invoiceId && i.accountRef === session.accountRef && i.status === "issued");
|
|
1435
|
-
if (!invoice?.document || invoice.number === null) return null;
|
|
1436
|
-
const safe = invoice.number.replace(/[^A-Za-z0-9._-]+/g, "_").replace(/^[._]+/, "").slice(0, 100);
|
|
1437
|
-
return {
|
|
1438
|
-
bytes: invoice.document.bytes,
|
|
1439
|
-
filename: `${safe === "" ? "invoice" : safe}.pdf`
|
|
1440
|
-
};
|
|
1441
|
-
}
|
|
1442
|
-
/** Requeues the event's delivery (dead or delivered included); `false` if there is no such event. */
|
|
1443
|
-
function redeliverEvent(w, id) {
|
|
1444
|
-
const event = w.events.find((e) => e.id === id);
|
|
1445
|
-
if (!event) return false;
|
|
1446
|
-
if (event.delivery) Object.assign(event.delivery, {
|
|
1447
|
-
status: "pending",
|
|
1448
|
-
attempts: 0,
|
|
1449
|
-
nextAttemptAt: w.now(),
|
|
1450
|
-
lastError: null,
|
|
1451
|
-
deliveredAt: null
|
|
1452
|
-
});
|
|
1453
|
-
return true;
|
|
1454
|
-
}
|
|
1455
|
-
/**
|
|
1456
|
-
* Tahsilat sonucu. Dunning'deki abonelikte AYNI başarısız siparişin yeniden
|
|
1457
|
-
* denemesidir (iyzico retry'ı; servisin `existingPayment` yolu), aksi halde
|
|
1458
|
-
* mevcut dönem sonundan başlayan yeni sipariş.
|
|
1459
|
-
*/
|
|
1460
|
-
function applyRenewalPayment(w, sub, outcome) {
|
|
1461
|
-
const now = w.now();
|
|
1462
|
-
const price = priceOf(w, sub.priceId);
|
|
1463
|
-
const retried = inDunning(sub) ? [...w.payments.filter((p) => p.subscriptionId === sub.id && p.status === "failed")].sort((a, b) => b.occurredAt - a.occurredAt)[0] : void 0;
|
|
1464
|
-
if (outcome === "failed") {
|
|
1465
|
-
let payment = retried;
|
|
1466
|
-
if (payment) Object.assign(payment, {
|
|
1467
|
-
status: "failed",
|
|
1468
|
-
attempt: payment.attempt + 1,
|
|
1469
|
-
occurredAt: now
|
|
1470
|
-
});
|
|
1471
|
-
else {
|
|
1472
|
-
const periodStart = sub.currentPeriodEnd;
|
|
1473
|
-
payment = insertPayment(w, sub, {
|
|
1474
|
-
providerOrderRef: `fake_ord_${globalThis.crypto.randomUUID()}`,
|
|
1475
|
-
status: "failed",
|
|
1476
|
-
periodStart,
|
|
1477
|
-
periodEnd: addInterval(periodStart ?? now, price.interval, price.intervalCount)
|
|
1478
|
-
});
|
|
1479
|
-
}
|
|
1480
|
-
const dunningStartedAt = sub.status === "active" ? now : sub.dunningStartedAt ?? now;
|
|
1481
|
-
if (sub.status === "active") Object.assign(sub, {
|
|
1482
|
-
status: "past_due",
|
|
1483
|
-
pastDueSince: now,
|
|
1484
|
-
dunningStartedAt: now,
|
|
1485
|
-
dunningStep: 0,
|
|
1486
|
-
nextDunningAt: now
|
|
1487
|
-
});
|
|
1488
|
-
const failures = w.payments.filter((p) => p.subscriptionId === sub.id && p.status === "failed" && p.createdAt >= dunningStartedAt).length;
|
|
1489
|
-
commitAccountChange(w, sub.accountRef, {
|
|
1490
|
-
type: "subscription.payment_failed",
|
|
1491
|
-
detail: {
|
|
1492
|
-
subscriptionId: sub.id,
|
|
1493
|
-
paymentId: payment.id,
|
|
1494
|
-
attempt: Math.max(1, failures),
|
|
1495
|
-
nextRetryAt: null
|
|
1496
|
-
}
|
|
1497
|
-
});
|
|
1498
|
-
return;
|
|
1499
|
-
}
|
|
1500
|
-
const periodStart = retried?.periodStart ?? sub.currentPeriodEnd ?? now;
|
|
1501
|
-
const periodEnd = retried?.periodEnd ?? addInterval(periodStart, price.interval, price.intervalCount);
|
|
1502
|
-
let payment = retried;
|
|
1503
|
-
if (payment) Object.assign(payment, {
|
|
1504
|
-
status: "succeeded",
|
|
1505
|
-
attempt: payment.attempt + 1,
|
|
1506
|
-
periodStart,
|
|
1507
|
-
periodEnd,
|
|
1508
|
-
occurredAt: now
|
|
1509
|
-
});
|
|
1510
|
-
else payment = insertPayment(w, sub, {
|
|
1511
|
-
providerOrderRef: `fake_ord_${globalThis.crypto.randomUUID()}`,
|
|
1512
|
-
status: "succeeded",
|
|
1513
|
-
periodStart,
|
|
1514
|
-
periodEnd
|
|
1515
|
-
});
|
|
1516
|
-
const invoiceId = createInvoiceForPayment(w, payment);
|
|
1517
|
-
const recovering = inDunning(sub);
|
|
1518
|
-
const advance = sub.currentPeriodEnd === null || periodEnd > sub.currentPeriodEnd;
|
|
1519
|
-
Object.assign(sub, {
|
|
1520
|
-
status: "active",
|
|
1521
|
-
...recovering ? {
|
|
1522
|
-
pastDueSince: null,
|
|
1523
|
-
dunningStartedAt: null,
|
|
1524
|
-
dunningStep: 0,
|
|
1525
|
-
nextDunningAt: null
|
|
1526
|
-
} : {},
|
|
1527
|
-
...advance ? {
|
|
1528
|
-
currentPeriodStart: periodStart,
|
|
1529
|
-
currentPeriodEnd: periodEnd
|
|
1530
|
-
} : {}
|
|
1531
|
-
});
|
|
1532
|
-
commitAccountChange(w, sub.accountRef, recovering ? {
|
|
1533
|
-
type: "subscription.reactivated",
|
|
1534
|
-
detail: {
|
|
1535
|
-
subscriptionId: sub.id,
|
|
1536
|
-
paymentId: payment.id
|
|
1537
|
-
}
|
|
1538
|
-
} : {
|
|
1539
|
-
type: "subscription.renewed",
|
|
1540
|
-
detail: {
|
|
1541
|
-
subscriptionId: sub.id,
|
|
1542
|
-
paymentId: payment.id,
|
|
1543
|
-
periodStart: iso(periodStart),
|
|
1544
|
-
periodEnd: iso(periodEnd),
|
|
1545
|
-
invoiceId
|
|
1546
|
-
}
|
|
1547
|
-
});
|
|
1548
|
-
}
|
|
1549
|
-
/** `runDunning` + `processOne`, sahte sağlayıcıda: yeniden deneme tahsilatı değiştirmez (sonuç `renewal` ile gelir). */
|
|
1550
|
-
function runDunning(w) {
|
|
1551
|
-
const now = w.now();
|
|
1552
|
-
const policy = w.dunningPolicy;
|
|
1553
|
-
const due = w.subscriptions.filter((s) => inDunning(s) && s.dunningStartedAt !== null && s.nextDunningAt !== null && s.nextDunningAt <= now).sort((a, b) => a.nextDunningAt - b.nextDunningAt);
|
|
1554
|
-
let applied = 0;
|
|
1555
|
-
for (const sub of due) {
|
|
1556
|
-
const start = sub.dunningStartedAt;
|
|
1557
|
-
const step = policy.steps[sub.dunningStep];
|
|
1558
|
-
if (!step) {
|
|
1559
|
-
sub.nextDunningAt = null;
|
|
1560
|
-
continue;
|
|
1561
|
-
}
|
|
1562
|
-
const dueAt = start + step.afterDays * DAY_MS;
|
|
1563
|
-
if (dueAt > now) {
|
|
1564
|
-
sub.nextDunningAt = dueAt;
|
|
1565
|
-
continue;
|
|
1566
|
-
}
|
|
1567
|
-
applied++;
|
|
1568
|
-
const dunningDay = Math.floor((now - start) / DAY_MS);
|
|
1569
|
-
const next = policy.steps[sub.dunningStep + 1];
|
|
1570
|
-
const advance = () => {
|
|
1571
|
-
sub.dunningStep += 1;
|
|
1572
|
-
sub.nextDunningAt = next ? start + next.afterDays * DAY_MS : null;
|
|
1573
|
-
};
|
|
1574
|
-
if (policy.dryRun || step.action === "retry_payment" || step.action === "suspend" && sub.status === "suspended") {
|
|
1575
|
-
if (!policy.dryRun && step.action === "retry_payment" && lastFailedPayment(w, sub)) sub.lastPaymentRetryAt = now;
|
|
1576
|
-
advance();
|
|
1577
|
-
commitAccountChange(w, sub.accountRef, "dunning.advanced");
|
|
1578
|
-
} else if (step.action === "suspend") {
|
|
1579
|
-
advance();
|
|
1580
|
-
sub.status = "suspended";
|
|
1581
|
-
commitAccountChange(w, sub.accountRef, {
|
|
1582
|
-
type: "subscription.suspended",
|
|
1583
|
-
detail: {
|
|
1584
|
-
subscriptionId: sub.id,
|
|
1585
|
-
dunningDay
|
|
1586
|
-
}
|
|
1587
|
-
});
|
|
1588
|
-
} else {
|
|
1589
|
-
advance();
|
|
1590
|
-
Object.assign(sub, {
|
|
1591
|
-
status: "canceled",
|
|
1592
|
-
pastDueSince: null,
|
|
1593
|
-
canceledAt: now,
|
|
1594
|
-
cancelAt: now,
|
|
1595
|
-
cancelReason: "nonpayment",
|
|
1596
|
-
nextDunningAt: null
|
|
1597
|
-
});
|
|
1598
|
-
commitAccountChange(w, sub.accountRef, {
|
|
1599
|
-
type: "subscription.terminated",
|
|
1600
|
-
detail: {
|
|
1601
|
-
subscriptionId: sub.id,
|
|
1602
|
-
reason: "nonpayment",
|
|
1603
|
-
dunningDay
|
|
1604
|
-
}
|
|
1605
|
-
});
|
|
1606
|
-
}
|
|
1607
|
-
}
|
|
1608
|
-
return applied;
|
|
1609
|
-
}
|
|
1610
|
-
function expireScheduledCancellations(w) {
|
|
1611
|
-
const now = w.now();
|
|
1612
|
-
for (const sub of w.subscriptions) {
|
|
1613
|
-
if (!isLive(sub) || !sub.cancelAtPeriodEnd || sub.cancelAt === null || sub.cancelAt > now) continue;
|
|
1614
|
-
Object.assign(sub, {
|
|
1615
|
-
status: "expired",
|
|
1616
|
-
pastDueSince: null,
|
|
1617
|
-
dunningStartedAt: null,
|
|
1618
|
-
dunningStep: 0,
|
|
1619
|
-
nextDunningAt: null
|
|
1620
|
-
});
|
|
1621
|
-
commitAccountChange(w, sub.accountRef, {
|
|
1622
|
-
type: "subscription.expired",
|
|
1623
|
-
detail: {
|
|
1624
|
-
subscriptionId: sub.id,
|
|
1625
|
-
effectiveAt: iso(sub.cancelAt)
|
|
1626
|
-
}
|
|
1627
|
-
});
|
|
1628
|
-
}
|
|
1629
|
-
}
|
|
1630
|
-
/**
|
|
1631
|
-
* Expired open session (formless: the fake hosted page has no provider form, so `expired`).
|
|
1632
|
-
* Servis gibi kapanan oturumun `expiresAt`'i job kirasının sonu (o an + 5 dk) kalır.
|
|
1633
|
-
*/
|
|
1634
|
-
function expireCheckoutSessions(w) {
|
|
1635
|
-
const now = w.now();
|
|
1636
|
-
for (const session of w.sessions) {
|
|
1637
|
-
if (session.status !== "open" || session.expiresAt > now) continue;
|
|
1638
|
-
Object.assign(session, {
|
|
1639
|
-
status: "expired",
|
|
1640
|
-
failureReason: "expired",
|
|
1641
|
-
completedAt: now,
|
|
1642
|
-
expiresAt: now + 3e5,
|
|
1643
|
-
...CLOSED_SESSION_CLEANUP
|
|
1644
|
-
});
|
|
1645
|
-
commitAccountChange(w, session.accountRef, {
|
|
1646
|
-
type: "checkout.expired",
|
|
1647
|
-
detail: { checkoutSessionId: session.id }
|
|
1648
|
-
});
|
|
1649
|
-
}
|
|
1650
|
-
}
|
|
1651
|
-
function applyGrantWindows(w) {
|
|
1652
|
-
const now = w.now();
|
|
1653
|
-
const refresh = /* @__PURE__ */ new Set();
|
|
1654
|
-
for (const g of w.grants) {
|
|
1655
|
-
if (g.revokedAt !== null) continue;
|
|
1656
|
-
const start = !g.startApplied && g.startsAt <= now;
|
|
1657
|
-
const end = !g.endApplied && g.endsAt !== null && g.endsAt <= now;
|
|
1658
|
-
if (!start && !end) continue;
|
|
1659
|
-
g.startApplied ||= g.startsAt <= now;
|
|
1660
|
-
g.endApplied ||= g.endsAt !== null && g.endsAt <= now;
|
|
1661
|
-
refresh.add(g.accountRef);
|
|
1662
|
-
}
|
|
1663
|
-
for (const ref of refresh) commitAccountChange(w, ref, "grant.window_applied");
|
|
1664
|
-
}
|
|
1665
|
-
/** `deleteExpiredPortalSessions` (K21): süresi geçen (sınır dahil) portal oturumları silinir; sayfa zaten reddeder. */
|
|
1666
|
-
function deleteExpiredPortalSessions(w) {
|
|
1667
|
-
const now = w.now();
|
|
1668
|
-
w.portalSessions = w.portalSessions.filter((s) => s.expiresAt > now);
|
|
1669
|
-
}
|
|
1670
|
-
/** Bir job turu (servis sırası: dunning, dönem sonu iptali, checkout süresi, grant penceresi, portal oturumu silme). */
|
|
1671
|
-
function runJobs(w) {
|
|
1672
|
-
for (let i = 0; i < 100 && runDunning(w) > 0; i++);
|
|
1673
|
-
expireScheduledCancellations(w);
|
|
1674
|
-
expireCheckoutSessions(w);
|
|
1675
|
-
applyGrantWindows(w);
|
|
1676
|
-
deleteExpiredPortalSessions(w);
|
|
1677
|
-
}
|
|
1678
|
-
/** Bir sonraki zamanlanmış iş anı (`after` sonrası, `until` dahil). */
|
|
1679
|
-
function nextJobAt(w, after, until) {
|
|
1680
|
-
const times = [
|
|
1681
|
-
...w.subscriptions.flatMap((s) => [inDunning(s) ? s.nextDunningAt : null, isLive(s) && s.cancelAtPeriodEnd ? s.cancelAt : null]),
|
|
1682
|
-
...w.sessions.map((s) => s.status === "open" ? s.expiresAt : null),
|
|
1683
|
-
...w.grants.flatMap((g) => g.revokedAt !== null ? [] : [g.startApplied ? null : g.startsAt, g.endApplied ? null : g.endsAt])
|
|
1684
|
-
];
|
|
1685
|
-
let next = null;
|
|
1686
|
-
for (const t of times) if (t !== null && t > after && t <= until && (next === null || t < next)) next = t;
|
|
1687
|
-
return next;
|
|
1688
|
-
}
|
|
1689
|
-
/** Saati `target`a ilerletir; her zamanlanmış iş kendi anında koşar. */
|
|
1690
|
-
function advanceTo(w, target) {
|
|
1691
|
-
runJobs(w);
|
|
1692
|
-
for (let guard = 0; guard < 1e4; guard++) {
|
|
1693
|
-
const next = nextJobAt(w, w.now(), target);
|
|
1694
|
-
if (next === null) break;
|
|
1695
|
-
w.setNow(next);
|
|
1696
|
-
runJobs(w);
|
|
1697
|
-
}
|
|
1698
|
-
w.setNow(target);
|
|
1699
|
-
runJobs(w);
|
|
1700
|
-
}
|
|
1701
|
-
//#endregion
|
|
1702
|
-
//#region src/testing/hosted.ts
|
|
1703
|
-
const HEADERS$1 = {
|
|
1704
|
-
"cache-control": "no-store",
|
|
1705
|
-
"referrer-policy": "no-referrer"
|
|
1706
|
-
};
|
|
1707
|
-
const escapeHtml = (value) => value.replace(/[&<>"']/g, (ch) => ({
|
|
1708
|
-
"&": "&",
|
|
1709
|
-
"<": "<",
|
|
1710
|
-
">": ">",
|
|
1711
|
-
"\"": """,
|
|
1712
|
-
"'": "'"
|
|
1713
|
-
})[ch]);
|
|
1714
|
-
const html = (status, body) => new Response(`<!doctype html><html lang="tr"><meta charset="utf-8"><title>Sahte steward ödeme sayfası</title><body>${body}</body></html>`, {
|
|
1715
|
-
status,
|
|
1716
|
-
headers: {
|
|
1717
|
-
...HEADERS$1,
|
|
1718
|
-
"content-type": "text/html; charset=utf-8"
|
|
1719
|
-
}
|
|
1720
|
-
});
|
|
1721
|
-
const notFound$2 = () => html(404, "<h1>Sayfa bulunamadı</h1><p>Bu ödeme bağlantısı geçersiz ya da süresi dolmuş.</p>");
|
|
1722
|
-
const redirect$1 = (location) => new Response(null, {
|
|
1723
|
-
status: 303,
|
|
1724
|
-
headers: {
|
|
1725
|
-
...HEADERS$1,
|
|
1726
|
-
location
|
|
1727
|
-
}
|
|
1728
|
-
});
|
|
1729
|
-
const PATH$1 = /^\/public\/v1\/checkout\/([^/]+)(?:\/(pay|cancel))?$/;
|
|
1730
|
-
function page(w, session, k) {
|
|
1731
|
-
const price = priceOf(w, session.priceId);
|
|
1732
|
-
const action = (name) => `${encodeURIComponent(session.id)}/${name}`;
|
|
1733
|
-
const key = `<input type="hidden" name="k" value="${escapeHtml(k)}">`;
|
|
1734
|
-
const docs = session.consentDocs.map((d) => `<li><a href="${escapeHtml(d.url)}">${escapeHtml(d.document)}</a> (${escapeHtml(d.version)})</li>`).join("");
|
|
1735
|
-
return html(200, `<h1>Sahte steward ödeme sayfası</h1>
|
|
1736
|
-
<p>Plan: <strong>${escapeHtml(price.planCode)}</strong> · ${escapeHtml(price.interval)} · ${price.amountMinor} ${escapeHtml(price.currency)} (kuruş)</p>
|
|
1737
|
-
<p>Hesap: ${escapeHtml(session.accountRef)} · dil: ${escapeHtml(session.locale)}</p>
|
|
1738
|
-
${docs ? `<p>Onaylanmış sayılan belgeler:</p><ul>${docs}</ul>` : ""}
|
|
1739
|
-
<form method="post" action="${action("pay")}">${key}<button name="outcome" value="succeeded">Tamamla</button> <button name="outcome" value="failed">Reddet</button></form>
|
|
1740
|
-
<form method="post" action="${action("cancel")}">${key}<button>Vazgeç</button></form>`);
|
|
1741
|
-
}
|
|
1742
|
-
/** `/public/*` istekleri (kimliksiz). Eşleşmeyen yol 404. */
|
|
1743
|
-
function handlePublic(w, req) {
|
|
1744
|
-
const match = PATH$1.exec(req.url.pathname);
|
|
1745
|
-
if (!match) return notFound$2();
|
|
1746
|
-
let id;
|
|
1747
|
-
try {
|
|
1748
|
-
id = decodeURIComponent(match[1]);
|
|
1749
|
-
} catch {
|
|
1750
|
-
return notFound$2();
|
|
1751
|
-
}
|
|
1752
|
-
const action = match[2];
|
|
1753
|
-
if (action === void 0) {
|
|
1754
|
-
if (req.method !== "GET") return notFound$2();
|
|
1755
|
-
const k = req.url.searchParams.get("k") ?? "";
|
|
1756
|
-
const session = openHostedSession(w, id, k);
|
|
1757
|
-
return session ? page(w, session, k) : notFound$2();
|
|
1758
|
-
}
|
|
1759
|
-
if (req.method !== "POST") return notFound$2();
|
|
1760
|
-
const form = new URLSearchParams(req.body);
|
|
1761
|
-
const k = form.get("k") ?? "";
|
|
1762
|
-
const session = openHostedSession(w, id, k);
|
|
1763
|
-
if (!session) return notFound$2();
|
|
1764
|
-
if (action === "cancel") {
|
|
1765
|
-
const target = cancelCheckout(w, session);
|
|
1766
|
-
return target ? redirect$1(target) : html(200, "<h1>Ödeme iptal edildi</h1><p>Sizden ücret alınmadı.</p>");
|
|
1767
|
-
}
|
|
1768
|
-
if (form.get("outcome") === "failed") failCheckout(w, session, "fake:card_declined");
|
|
1769
|
-
else completeCheckout(w, session);
|
|
1770
|
-
const status = session.status === "completed" ? "success" : "failed";
|
|
1771
|
-
return redirect$1(checkoutRedirectUrl(session, status));
|
|
1772
|
-
}
|
|
1773
|
-
//#endregion
|
|
1774
|
-
//#region src/testing/admin.ts
|
|
1775
|
-
/** Servis gövde şeması + üretim dışı adres politikası. */
|
|
1776
|
-
const CreateEndpointInputSchema = WebhookEndpointCreateInputSchema.superRefine((input, ctx) => {
|
|
1777
|
-
const problem = webhookEndpointUrlProblem(input.url, { allowLoopback: true });
|
|
1778
|
-
if (problem !== null) ctx.addIssue({
|
|
1779
|
-
code: "custom",
|
|
1780
|
-
path: ["url"],
|
|
1781
|
-
message: problem
|
|
1782
|
-
});
|
|
1783
|
-
});
|
|
1784
|
-
function settings(w) {
|
|
1785
|
-
return w.settings;
|
|
1786
|
-
}
|
|
1787
|
-
function patchSettings(w, patch) {
|
|
1788
|
-
w.settings = applySettingsPatch(w.settings, patch);
|
|
1789
|
-
if (patch.appUrl !== void 0 || patch.extraReturnOrigins !== void 0) w.permissiveReturnOrigins = false;
|
|
1790
|
-
return w.settings;
|
|
1791
|
-
}
|
|
1792
|
-
function toEndpoint(row) {
|
|
1793
|
-
return {
|
|
1794
|
-
id: row.id,
|
|
1795
|
-
url: row.url,
|
|
1796
|
-
status: row.status,
|
|
1797
|
-
eventTypes: [...row.eventTypes],
|
|
1798
|
-
description: row.description,
|
|
1799
|
-
createdAt: iso(row.createdAt),
|
|
1800
|
-
rotatedAt: isoOrNull(row.rotatedAt),
|
|
1801
|
-
lastDelivery: null
|
|
1802
|
-
};
|
|
1803
|
-
}
|
|
1804
|
-
function listEndpoints(w) {
|
|
1805
|
-
return { items: w.adminEndpoints.map(toEndpoint) };
|
|
1806
|
-
}
|
|
1807
|
-
function createEndpoint(w, input) {
|
|
1808
|
-
const row = {
|
|
1809
|
-
id: w.id(),
|
|
1810
|
-
url: new URL(input.url).toString(),
|
|
1811
|
-
status: "active",
|
|
1812
|
-
eventTypes: [...input.eventTypes ?? DEFAULT_ENDPOINT_EVENT_TYPES],
|
|
1813
|
-
description: input.description ?? null,
|
|
1814
|
-
createdAt: w.now(),
|
|
1815
|
-
rotatedAt: null
|
|
1816
|
-
};
|
|
1817
|
-
w.adminEndpoints.push(row);
|
|
1818
|
-
return toEndpoint(row);
|
|
1819
|
-
}
|
|
1820
|
-
const notFound$1 = {
|
|
1821
|
-
ok: false,
|
|
1822
|
-
code: "webhook_endpoint_not_found"
|
|
1823
|
-
};
|
|
1824
|
-
function rotateEndpoint(w, id, _input) {
|
|
1825
|
-
const row = w.adminEndpoints.find((e) => e.id === id);
|
|
1826
|
-
if (!row) return notFound$1;
|
|
1827
|
-
row.rotatedAt = w.now();
|
|
1828
|
-
return {
|
|
1829
|
-
ok: true,
|
|
1830
|
-
value: toEndpoint(row)
|
|
1831
|
-
};
|
|
1832
|
-
}
|
|
1833
|
-
function deactivateEndpoint(w, id) {
|
|
1834
|
-
const row = w.adminEndpoints.find((e) => e.id === id);
|
|
1835
|
-
if (!row) return notFound$1;
|
|
1836
|
-
row.status = "inactive";
|
|
1837
|
-
return {
|
|
1838
|
-
ok: true,
|
|
1839
|
-
value: toEndpoint(row)
|
|
1840
|
-
};
|
|
1841
|
-
}
|
|
1842
|
-
//#endregion
|
|
1843
|
-
//#region src/testing/doctor.ts
|
|
1844
|
-
function doctorReport(w) {
|
|
1845
|
-
const catalog = catalogOf(w);
|
|
1846
|
-
const unsellablePrices = [];
|
|
1847
|
-
let sellablePrices = 0;
|
|
1848
|
-
const prices = w.prices.filter((p) => p.active && w.plans.get(p.planCode)?.active === true && w.plans.get(p.planCode)?.sellable === true).sort((a, b) => {
|
|
1849
|
-
const pa = w.plans.get(a.planCode);
|
|
1850
|
-
const pb = w.plans.get(b.planCode);
|
|
1851
|
-
return pa.rank - pb.rank || cmp(a.planCode, b.planCode) || cmp(a.interval, b.interval) || a.intervalCount - b.intervalCount || cmp(a.currency, b.currency);
|
|
1852
|
-
});
|
|
1853
|
-
for (const p of prices) if (p.intervalCount === 1) sellablePrices++;
|
|
1854
|
-
else unsellablePrices.push({
|
|
1855
|
-
priceId: p.id,
|
|
1856
|
-
planCode: p.planCode,
|
|
1857
|
-
interval: p.interval,
|
|
1858
|
-
intervalCount: p.intervalCount,
|
|
1859
|
-
currency: p.currency,
|
|
1860
|
-
reason: "interval_count_unsupported"
|
|
1861
|
-
});
|
|
1862
|
-
return buildDoctorReport({
|
|
1863
|
-
checkedAt: new Date(w.now()),
|
|
1864
|
-
product: {
|
|
1865
|
-
code: w.productCode,
|
|
1866
|
-
name: w.productCode
|
|
1867
|
-
},
|
|
1868
|
-
catalog: {
|
|
1869
|
-
synced: catalog !== null,
|
|
1870
|
-
version: catalog ? catalogVersionOf(catalog) : null,
|
|
1871
|
-
plans: catalog?.plans.length ?? 0,
|
|
1872
|
-
sellablePrices,
|
|
1873
|
-
unsellablePrices
|
|
1874
|
-
},
|
|
1875
|
-
provider: {
|
|
1876
|
-
id: w.settings.provider,
|
|
1877
|
-
configured: true,
|
|
1878
|
-
environment: "fake",
|
|
1879
|
-
merchantId: null,
|
|
1880
|
-
healthy: true,
|
|
1881
|
-
lastError: null
|
|
1882
|
-
},
|
|
1883
|
-
settings: w.settings,
|
|
1884
|
-
endpoints: w.adminEndpoints.filter((e) => e.status !== "inactive").map((e) => ({
|
|
1885
|
-
id: e.id,
|
|
1886
|
-
url: e.url,
|
|
1887
|
-
status: e.status,
|
|
1888
|
-
eventTypes: [...e.eventTypes],
|
|
1889
|
-
pending: 0,
|
|
1890
|
-
dead: 0,
|
|
1891
|
-
oldestPendingAt: null,
|
|
1892
|
-
lastDelivery: null
|
|
1893
|
-
})),
|
|
1894
|
-
dunning: { dryRun: w.dunningPolicy.dryRun }
|
|
1895
|
-
});
|
|
1896
|
-
}
|
|
1897
|
-
const cmp = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
1898
|
-
//#endregion
|
|
1899
|
-
//#region src/testing/invoices.ts
|
|
1900
|
-
function createManualInvoice(w, ref, input) {
|
|
1901
|
-
const profile = w.accounts.get(ref)?.profile;
|
|
1902
|
-
if (!w.accounts.has(ref)) return {
|
|
1903
|
-
ok: false,
|
|
1904
|
-
code: "account_not_found"
|
|
1905
|
-
};
|
|
1906
|
-
if (!profile) return {
|
|
1907
|
-
ok: false,
|
|
1908
|
-
code: "billing_profile_required"
|
|
1909
|
-
};
|
|
1910
|
-
const totals = manualInvoiceTotals(input);
|
|
1911
|
-
const invoice = {
|
|
1912
|
-
id: w.id(),
|
|
1913
|
-
accountRef: ref,
|
|
1914
|
-
subscriptionId: null,
|
|
1915
|
-
paymentId: null,
|
|
1916
|
-
status: "draft",
|
|
1917
|
-
number: null,
|
|
1918
|
-
currency: input.currency,
|
|
1919
|
-
subtotalMinor: totals.subtotalMinor,
|
|
1920
|
-
taxMinor: totals.taxMinor,
|
|
1921
|
-
totalMinor: totals.totalMinor,
|
|
1922
|
-
taxRateBps: input.taxRateBps,
|
|
1923
|
-
buyer: profile,
|
|
1924
|
-
lines: totals.lines,
|
|
1925
|
-
externalRef: null,
|
|
1926
|
-
issuedAt: null,
|
|
1927
|
-
createdAt: w.now(),
|
|
1928
|
-
document: null,
|
|
1929
|
-
dueAt: input.dueAt !== void 0 ? Date.parse(input.dueAt) : null,
|
|
1930
|
-
note: input.note ?? null,
|
|
1931
|
-
voidedAt: null,
|
|
1932
|
-
voidReason: null
|
|
1933
|
-
};
|
|
1934
|
-
w.invoices.push(invoice);
|
|
1935
|
-
const { state } = commitAccountChange(w, ref, {
|
|
1936
|
-
type: "invoice.created",
|
|
1937
|
-
detail: {
|
|
1938
|
-
invoiceId: invoice.id,
|
|
1939
|
-
totalMinor: totals.totalMinor,
|
|
1940
|
-
currency: input.currency
|
|
1941
|
-
}
|
|
1942
|
-
});
|
|
1943
|
-
return {
|
|
1944
|
-
ok: true,
|
|
1945
|
-
value: {
|
|
1946
|
-
...toInvoice(invoice),
|
|
1947
|
-
account: state
|
|
1948
|
-
}
|
|
1949
|
-
};
|
|
1950
|
-
}
|
|
1951
|
-
function voidInvoice(w, id, input) {
|
|
1952
|
-
const invoice = w.invoices.find((i) => i.id === id);
|
|
1953
|
-
if (!invoice) return {
|
|
1954
|
-
ok: false,
|
|
1955
|
-
code: "invoice_not_found"
|
|
1956
|
-
};
|
|
1957
|
-
if (invoice.status === "void") return {
|
|
1958
|
-
ok: false,
|
|
1959
|
-
code: "invoice_not_voidable"
|
|
1960
|
-
};
|
|
1961
|
-
if (invoice.paymentId !== null) return {
|
|
1962
|
-
ok: false,
|
|
1963
|
-
code: "invoice_has_payment"
|
|
1964
|
-
};
|
|
1965
|
-
Object.assign(invoice, {
|
|
1966
|
-
status: "void",
|
|
1967
|
-
voidedAt: w.now(),
|
|
1968
|
-
voidReason: input.reason
|
|
1969
|
-
});
|
|
1970
|
-
const { state } = commitAccountChange(w, invoice.accountRef, {
|
|
1971
|
-
type: "invoice.voided",
|
|
1972
|
-
detail: {
|
|
1973
|
-
invoiceId: id,
|
|
1974
|
-
number: invoice.number,
|
|
1975
|
-
totalMinor: invoice.totalMinor,
|
|
1976
|
-
currency: invoice.currency,
|
|
1977
|
-
reason: input.reason
|
|
1978
|
-
}
|
|
1979
|
-
});
|
|
1980
|
-
return {
|
|
1981
|
-
ok: true,
|
|
1982
|
-
value: {
|
|
1983
|
-
...toInvoice(invoice),
|
|
1984
|
-
account: state
|
|
1985
|
-
}
|
|
1986
|
-
};
|
|
1987
|
-
}
|
|
1988
|
-
//#endregion
|
|
1989
|
-
//#region src/testing/http.ts
|
|
1990
|
-
const MUTATING = /* @__PURE__ */ new Set([
|
|
1991
|
-
"POST",
|
|
1992
|
-
"PUT",
|
|
1993
|
-
"PATCH",
|
|
1994
|
-
"DELETE"
|
|
1995
|
-
]);
|
|
1996
|
-
const KEY_RE = /^[A-Za-z0-9_.:-]{8,200}$/;
|
|
1997
|
-
const json = (body, status = 200, headers = {}) => ({
|
|
1998
|
-
status,
|
|
1999
|
-
body: JSON.stringify(body),
|
|
2000
|
-
headers: {
|
|
2001
|
-
"content-type": "application/json",
|
|
2002
|
-
...headers
|
|
2003
|
-
}
|
|
2004
|
-
});
|
|
2005
|
-
const empty = (status, headers = {}) => ({
|
|
2006
|
-
status,
|
|
2007
|
-
body: null,
|
|
2008
|
-
headers
|
|
2009
|
-
});
|
|
2010
|
-
/** `If-None-Match` (liste, zayıf ETag ve `*` dahil) verilen ETag'i karşılıyor mu. */
|
|
2011
|
-
function notModified(header, etag) {
|
|
2012
|
-
if (!header) return false;
|
|
2013
|
-
return header.split(",").some((tag) => {
|
|
2014
|
-
const value = tag.trim();
|
|
2015
|
-
return value === "*" || value.replace(/^W\//, "") === etag;
|
|
2016
|
-
});
|
|
2017
|
-
}
|
|
2018
|
-
function parseBody(ctx, schema) {
|
|
2019
|
-
let value;
|
|
2020
|
-
try {
|
|
2021
|
-
value = JSON.parse(ctx.body);
|
|
2022
|
-
} catch {
|
|
2023
|
-
return {
|
|
2024
|
-
ok: false,
|
|
2025
|
-
reply: ctx.error("invalid_json")
|
|
2026
|
-
};
|
|
2027
|
-
}
|
|
2028
|
-
const parsed = schema.safeParse(value);
|
|
2029
|
-
if (parsed.success) return {
|
|
2030
|
-
ok: true,
|
|
2031
|
-
data: parsed.data
|
|
2032
|
-
};
|
|
2033
|
-
const message = parsed.error.issues.slice(0, 10).map((i) => `${i.path.join(".") || "(kök)"}: ${i.message}`).join("; ");
|
|
2034
|
-
return {
|
|
2035
|
-
ok: false,
|
|
2036
|
-
reply: ctx.error("invalid_request", message)
|
|
2037
|
-
};
|
|
2038
|
-
}
|
|
2039
|
-
const failure = (ctx, result) => ctx.error(result.code, result.message);
|
|
2040
|
-
function routes(w, config) {
|
|
2041
|
-
const route = (method, path, handle) => ({
|
|
2042
|
-
method,
|
|
2043
|
-
path: path.split("/").filter(Boolean),
|
|
2044
|
-
handle
|
|
2045
|
-
});
|
|
2046
|
-
const ref = (ctx) => ExternalRefSchema.safeParse(ctx.params.ref).success ? ctx.params.ref : null;
|
|
2047
|
-
const admin = (handle) => (ctx) => ctx.auth.scope === "admin" ? handle(ctx) : ctx.error("forbidden", "admin kapsamlı anahtar gerekli");
|
|
2048
|
-
return [
|
|
2049
|
-
route("GET", "me", (ctx) => json({
|
|
2050
|
-
product: w.productCode,
|
|
2051
|
-
keyName: ctx.auth.keyName,
|
|
2052
|
-
scope: ctx.auth.scope
|
|
2053
|
-
})),
|
|
2054
|
-
route("GET", "catalog", (ctx) => {
|
|
2055
|
-
const catalog = catalogOf(w);
|
|
2056
|
-
if (!catalog) return ctx.error("catalog_not_configured", "ürün için henüz katalog senkronlanmadı");
|
|
2057
|
-
const etag = `"${catalogVersionOf(catalog)}"`;
|
|
2058
|
-
if (ctx.headers.get("if-none-match") === etag) return empty(304, { etag });
|
|
2059
|
-
return json(catalog, 200, { etag });
|
|
2060
|
-
}),
|
|
2061
|
-
route("PUT", "admin/catalog", admin((ctx) => {
|
|
2062
|
-
const parsed = parseBody(ctx, CatalogSyncInputSchema);
|
|
2063
|
-
if (!parsed.ok) return parsed.reply;
|
|
2064
|
-
const result = syncCatalog(w, parsed.data);
|
|
2065
|
-
return result.ok ? json(result.value) : failure(ctx, result);
|
|
2066
|
-
})),
|
|
2067
|
-
route("POST", "admin/accounts/refresh", admin(() => json(refreshAccounts(w)))),
|
|
2068
|
-
route("GET", "admin/settings", admin(() => json(settings(w)))),
|
|
2069
|
-
route("PATCH", "admin/settings", admin((ctx) => {
|
|
2070
|
-
const parsed = parseBody(ctx, ProductSettingsPatchSchema);
|
|
2071
|
-
return parsed.ok ? json(patchSettings(w, parsed.data)) : parsed.reply;
|
|
2072
|
-
})),
|
|
2073
|
-
route("GET", "admin/webhook-endpoints", admin(() => json(listEndpoints(w)))),
|
|
2074
|
-
route("POST", "admin/webhook-endpoints", admin((ctx) => {
|
|
2075
|
-
const parsed = parseBody(ctx, CreateEndpointInputSchema);
|
|
2076
|
-
return parsed.ok ? json(createEndpoint(w, parsed.data), 201) : parsed.reply;
|
|
2077
|
-
})),
|
|
2078
|
-
route("POST", "admin/webhook-endpoints/:id/rotate", admin((ctx) => {
|
|
2079
|
-
if (!IdSchema.safeParse(ctx.params.id).success) return ctx.error("webhook_endpoint_not_found");
|
|
2080
|
-
const parsed = parseBody(ctx, WebhookEndpointRotateInputSchema);
|
|
2081
|
-
if (!parsed.ok) return parsed.reply;
|
|
2082
|
-
const result = rotateEndpoint(w, ctx.params.id, parsed.data);
|
|
2083
|
-
return result.ok ? json(result.value) : failure(ctx, result);
|
|
2084
|
-
})),
|
|
2085
|
-
route("POST", "admin/webhook-endpoints/:id/deactivate", admin((ctx) => {
|
|
2086
|
-
if (!IdSchema.safeParse(ctx.params.id).success) return ctx.error("webhook_endpoint_not_found");
|
|
2087
|
-
const result = deactivateEndpoint(w, ctx.params.id);
|
|
2088
|
-
return result.ok ? json(result.value) : failure(ctx, result);
|
|
2089
|
-
})),
|
|
2090
|
-
route("PUT", "accounts/:ref", (ctx) => {
|
|
2091
|
-
const accountRef = ref(ctx);
|
|
2092
|
-
if (!accountRef) return ctx.error("invalid_account_ref");
|
|
2093
|
-
const parsed = parseBody(ctx, AccountUpsertInputSchema);
|
|
2094
|
-
if (!parsed.ok) return parsed.reply;
|
|
2095
|
-
const { account, created } = upsertAccount(w, accountRef, parsed.data);
|
|
2096
|
-
return json(account, created ? 201 : 200);
|
|
2097
|
-
}),
|
|
2098
|
-
route("DELETE", "accounts/:ref", (ctx) => {
|
|
2099
|
-
const accountRef = ref(ctx);
|
|
2100
|
-
if (!accountRef) return ctx.error("invalid_account_ref");
|
|
2101
|
-
const result = deleteAccount(w, accountRef, ctx.actorRef);
|
|
2102
|
-
return result.ok ? json(result.value) : failure(ctx, result);
|
|
2103
|
-
}),
|
|
2104
|
-
route("GET", "accounts/:ref", (ctx) => {
|
|
2105
|
-
const accountRef = ref(ctx);
|
|
2106
|
-
if (!accountRef) return ctx.error("invalid_account_ref");
|
|
2107
|
-
const requested = (ctx.url.searchParams.get("include") ?? "").split(",").filter(Boolean);
|
|
2108
|
-
const unknown = requested.filter((i) => !ACCOUNT_INCLUDES.includes(i));
|
|
2109
|
-
if (unknown.length > 0) return ctx.error("invalid_include", unknown.join(","));
|
|
2110
|
-
const detail = accountDetail(w, accountRef, requested);
|
|
2111
|
-
return detail ? json(detail) : ctx.error("account_not_found");
|
|
2112
|
-
}),
|
|
2113
|
-
route("GET", "accounts/:ref/state", (ctx) => {
|
|
2114
|
-
const accountRef = ref(ctx);
|
|
2115
|
-
if (!accountRef) return ctx.error("invalid_account_ref");
|
|
2116
|
-
if (!w.accounts.has(accountRef)) return ctx.error("account_not_found");
|
|
2117
|
-
const state = storedState(w, accountRef);
|
|
2118
|
-
const etag = `"${state.version}"`;
|
|
2119
|
-
if (notModified(ctx.headers.get("if-none-match"), etag)) return empty(304, { etag });
|
|
2120
|
-
return json(state, 200, { etag });
|
|
2121
|
-
}),
|
|
2122
|
-
route("PUT", "accounts/:ref/plan", (ctx) => {
|
|
2123
|
-
const accountRef = ref(ctx);
|
|
2124
|
-
if (!accountRef) return ctx.error("invalid_account_ref");
|
|
2125
|
-
const parsed = parseBody(ctx, PlanSetInputSchema);
|
|
2126
|
-
if (!parsed.ok) return parsed.reply;
|
|
2127
|
-
const result = setPlan(w, accountRef, parsed.data, ctx.actorRef);
|
|
2128
|
-
return result.ok ? json(result.value) : failure(ctx, result);
|
|
2129
|
-
}),
|
|
2130
|
-
route("POST", "accounts/:ref/resync", (ctx) => {
|
|
2131
|
-
const accountRef = ref(ctx);
|
|
2132
|
-
if (!accountRef) return ctx.error("invalid_account_ref");
|
|
2133
|
-
const result = resyncAccount(w, accountRef);
|
|
2134
|
-
return result.ok ? json(result.value, 202) : failure(ctx, result);
|
|
2135
|
-
}),
|
|
2136
|
-
route("POST", "accounts/:ref/checkout-sessions", (ctx) => {
|
|
2137
|
-
const accountRef = ref(ctx);
|
|
2138
|
-
if (!accountRef) return ctx.error("invalid_account_ref");
|
|
2139
|
-
const parsed = parseBody(ctx, CheckoutSessionCreateInputSchema);
|
|
2140
|
-
if (!parsed.ok) return parsed.reply;
|
|
2141
|
-
const result = createCheckoutSession(w, config.baseUrl, accountRef, parsed.data, ctx.actorRef);
|
|
2142
|
-
return result.ok ? json(result.value, 201) : failure(ctx, result);
|
|
2143
|
-
}),
|
|
2144
|
-
route("POST", "accounts/:ref/portal-sessions", (ctx) => {
|
|
2145
|
-
const accountRef = ref(ctx);
|
|
2146
|
-
if (!accountRef) return ctx.error("invalid_account_ref");
|
|
2147
|
-
const parsed = ctx.body.trim() === "" ? {
|
|
2148
|
-
ok: true,
|
|
2149
|
-
data: {}
|
|
2150
|
-
} : parseBody(ctx, PortalSessionCreateInputSchema);
|
|
2151
|
-
if (!parsed.ok) return parsed.reply;
|
|
2152
|
-
const result = createPortalSession(w, config.baseUrl, accountRef, parsed.data, ctx.actorRef);
|
|
2153
|
-
return result.ok ? json(result.value, 201) : failure(ctx, result);
|
|
2154
|
-
}),
|
|
2155
|
-
route("GET", "checkout-sessions/:id", (ctx) => {
|
|
2156
|
-
const session = IdSchema.safeParse(ctx.params.id).success ? checkoutSessionResponse(w, ctx.params.id) : null;
|
|
2157
|
-
return session ? json(session) : ctx.error("checkout_session_not_found");
|
|
2158
|
-
}),
|
|
2159
|
-
route("POST", "subscriptions/:id/cancel", (ctx) => {
|
|
2160
|
-
if (!IdSchema.safeParse(ctx.params.id).success) return ctx.error("subscription_not_found");
|
|
2161
|
-
const parsed = parseBody(ctx, CancelSubscriptionInputSchema);
|
|
2162
|
-
if (!parsed.ok) return parsed.reply;
|
|
2163
|
-
const result = cancelSubscription(w, ctx.params.id, parsed.data, ctx.actorRef);
|
|
2164
|
-
return result.ok ? json(result.value) : failure(ctx, result);
|
|
2165
|
-
}),
|
|
2166
|
-
route("POST", "accounts/:ref/grants", (ctx) => {
|
|
2167
|
-
const accountRef = ref(ctx);
|
|
2168
|
-
if (!accountRef) return ctx.error("invalid_account_ref");
|
|
2169
|
-
const parsed = parseBody(ctx, GrantCreateInputSchema);
|
|
2170
|
-
if (!parsed.ok) return parsed.reply;
|
|
2171
|
-
const result = createGrant(w, accountRef, parsed.data, ctx.actorRef);
|
|
2172
|
-
return result.ok ? json(result.value, 201) : failure(ctx, result);
|
|
2173
|
-
}),
|
|
2174
|
-
route("DELETE", "grants/:id", (ctx) => IdSchema.safeParse(ctx.params.id).success && revokeGrant(w, ctx.params.id) ? empty(204) : ctx.error("grant_not_found")),
|
|
2175
|
-
route("GET", "stats", () => json(stats(w))),
|
|
2176
|
-
route("GET", "doctor", () => json(doctorReport(w), 200, { "cache-control": "no-store" })),
|
|
2177
|
-
route("POST", "invoices/:id/issue", (ctx) => {
|
|
2178
|
-
if (!IdSchema.safeParse(ctx.params.id).success) return ctx.error("not_found");
|
|
2179
|
-
const parsed = parseBody(ctx, InvoiceIssueInputSchema);
|
|
2180
|
-
if (!parsed.ok) return parsed.reply;
|
|
2181
|
-
const result = issueInvoice(w, ctx.params.id, parsed.data);
|
|
2182
|
-
return result.ok ? json(result.value) : failure(ctx, result);
|
|
2183
|
-
}),
|
|
2184
|
-
route("POST", "accounts/:ref/invoices", (ctx) => {
|
|
2185
|
-
const accountRef = ref(ctx);
|
|
2186
|
-
if (!accountRef) return ctx.error("invalid_account_ref");
|
|
2187
|
-
const parsed = parseBody(ctx, ManualInvoiceInputSchema);
|
|
2188
|
-
if (!parsed.ok) return parsed.reply;
|
|
2189
|
-
const result = createManualInvoice(w, accountRef, parsed.data);
|
|
2190
|
-
return result.ok ? json(result.value, 201) : failure(ctx, result);
|
|
2191
|
-
}),
|
|
2192
|
-
route("POST", "invoices/:id/void", (ctx) => {
|
|
2193
|
-
if (!IdSchema.safeParse(ctx.params.id).success) return ctx.error("not_found");
|
|
2194
|
-
const parsed = parseBody(ctx, InvoiceVoidInputSchema);
|
|
2195
|
-
if (!parsed.ok) return parsed.reply;
|
|
2196
|
-
const result = voidInvoice(w, ctx.params.id, parsed.data);
|
|
2197
|
-
return result.ok ? json(result.value) : failure(ctx, result);
|
|
2198
|
-
}),
|
|
2199
|
-
route("GET", "invoices/:id/document", (ctx) => {
|
|
2200
|
-
const invoice = IdSchema.safeParse(ctx.params.id).success ? w.invoices.find((i) => i.id === ctx.params.id) : void 0;
|
|
2201
|
-
if (!invoice?.document) return ctx.error("document_not_found");
|
|
2202
|
-
const { bytes, sha256 } = invoice.document;
|
|
2203
|
-
return {
|
|
2204
|
-
status: 200,
|
|
2205
|
-
body: bytes,
|
|
2206
|
-
headers: {
|
|
2207
|
-
"content-type": "application/pdf",
|
|
2208
|
-
"content-length": String(bytes.length),
|
|
2209
|
-
"cache-control": "private, no-store",
|
|
2210
|
-
"x-content-sha256": sha256
|
|
2211
|
-
}
|
|
2212
|
-
};
|
|
2213
|
-
})
|
|
2214
|
-
];
|
|
2215
|
-
}
|
|
2216
|
-
function decode(segment) {
|
|
2217
|
-
try {
|
|
2218
|
-
return decodeURIComponent(segment);
|
|
2219
|
-
} catch {
|
|
2220
|
-
return segment;
|
|
2221
|
-
}
|
|
2222
|
-
}
|
|
2223
|
-
function match(route, method, segments) {
|
|
2224
|
-
if (route.method !== method || route.path.length !== segments.length) return null;
|
|
2225
|
-
const params = {};
|
|
2226
|
-
for (const [i, part] of route.path.entries()) {
|
|
2227
|
-
const segment = segments[i];
|
|
2228
|
-
if (part.startsWith(":")) {
|
|
2229
|
-
if (segment === "") return null;
|
|
2230
|
-
params[part.slice(1)] = decode(segment);
|
|
2231
|
-
} else if (part !== segment) return null;
|
|
2232
|
-
}
|
|
2233
|
-
return params;
|
|
2234
|
-
}
|
|
2235
|
-
function createHttpHandler(w, config) {
|
|
2236
|
-
const table = routes(w, config);
|
|
2237
|
-
const idempotency = /* @__PURE__ */ new Map();
|
|
2238
|
-
return (req) => {
|
|
2239
|
-
const requestId = config.requestId();
|
|
2240
|
-
const error = (code, message) => {
|
|
2241
|
-
const spec = ERROR_CODES[code];
|
|
2242
|
-
const body = {
|
|
2243
|
-
error: code,
|
|
2244
|
-
...message !== void 0 ? { message } : {},
|
|
2245
|
-
category: spec.category,
|
|
2246
|
-
retryable: spec.retryable,
|
|
2247
|
-
requestId
|
|
2248
|
-
};
|
|
2249
|
-
return json(body, spec.httpStatus);
|
|
2250
|
-
};
|
|
2251
|
-
const reply = run(req, error);
|
|
2252
|
-
const headers = new Headers(reply.headers);
|
|
2253
|
-
headers.set(HEADER_REQUEST_ID, requestId);
|
|
2254
|
-
return new Response(reply.body, {
|
|
2255
|
-
status: reply.status,
|
|
2256
|
-
headers
|
|
2257
|
-
});
|
|
2258
|
-
};
|
|
2259
|
-
function run(req, error) {
|
|
2260
|
-
const path = req.url.pathname;
|
|
2261
|
-
if (path === "/health" && req.method === "GET") return json({ ok: true });
|
|
2262
|
-
if (path !== "/v1" && !path.startsWith("/v1/")) return error("not_found");
|
|
2263
|
-
const header = req.headers.get("authorization") ?? "";
|
|
2264
|
-
const token = header.startsWith("Bearer ") ? header.slice(7).trim() : "";
|
|
2265
|
-
const auth = token ? config.keys.get(token) : void 0;
|
|
2266
|
-
if (!auth) return error("unauthorized");
|
|
2267
|
-
const actorRef = req.headers.get(HEADER_ACTOR_REF);
|
|
2268
|
-
if (actorRef !== null && !ActorRefSchema.safeParse(actorRef).success) return error("invalid_actor_ref", "biçim: tür[:kimlik], ör. user:42");
|
|
2269
|
-
if (actorRef === null && MUTATING.has(req.method)) return error("actor_ref_required", `${HEADER_ACTOR_REF} başlığı zorunlu`);
|
|
2270
|
-
const segments = path.slice(3).split("/").slice(1);
|
|
2271
|
-
const dispatch = () => {
|
|
2272
|
-
if (segments[0] === "accounts" && segments[1] && w.accounts.get(decode(segments[1]))?.deletedAt !== void 0) return error("account_deleted");
|
|
2273
|
-
for (const route of table) {
|
|
2274
|
-
const params = match(route, req.method, segments);
|
|
2275
|
-
if (params) return route.handle({
|
|
2276
|
-
...req,
|
|
2277
|
-
params,
|
|
2278
|
-
auth,
|
|
2279
|
-
actorRef,
|
|
2280
|
-
error
|
|
2281
|
-
});
|
|
2282
|
-
}
|
|
2283
|
-
return error("not_found");
|
|
2284
|
-
};
|
|
2285
|
-
const guarded = () => {
|
|
2286
|
-
try {
|
|
2287
|
-
return dispatch();
|
|
2288
|
-
} catch (cause) {
|
|
2289
|
-
config.logger.error(`sahte steward ${req.method} ${path}: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
2290
|
-
return error("internal_error");
|
|
2291
|
-
}
|
|
2292
|
-
};
|
|
2293
|
-
if (!MUTATING.has(req.method)) return guarded();
|
|
2294
|
-
const key = req.headers.get(HEADER_IDEMPOTENCY_KEY);
|
|
2295
|
-
if (!key) return error("idempotency_key_required", `${HEADER_IDEMPOTENCY_KEY} başlığı zorunlu`);
|
|
2296
|
-
if (!KEY_RE.test(key)) return error("invalid_idempotency_key");
|
|
2297
|
-
const hash = `${req.method} ${path}${req.url.search}\n${req.body}`;
|
|
2298
|
-
const existing = idempotency.get(key);
|
|
2299
|
-
if (existing) {
|
|
2300
|
-
if (existing.hash !== hash) return error("idempotency_key_reused", "bu anahtar farklı bir istekte kullanıldı");
|
|
2301
|
-
if (existing.status === null) return error("request_in_progress");
|
|
2302
|
-
const body = existing.status === 204 ? null : existing.body;
|
|
2303
|
-
return {
|
|
2304
|
-
status: existing.status,
|
|
2305
|
-
body,
|
|
2306
|
-
headers: {
|
|
2307
|
-
"content-type": "application/json",
|
|
2308
|
-
"idempotent-replayed": "true"
|
|
2309
|
-
}
|
|
2310
|
-
};
|
|
2311
|
-
}
|
|
2312
|
-
idempotency.set(key, {
|
|
2313
|
-
hash,
|
|
2314
|
-
status: null,
|
|
2315
|
-
body: ""
|
|
2316
|
-
});
|
|
2317
|
-
const result = guarded();
|
|
2318
|
-
if (result.status < 200 || result.status >= 300) idempotency.delete(key);
|
|
2319
|
-
else {
|
|
2320
|
-
const text = typeof result.body === "string" ? result.body : "";
|
|
2321
|
-
idempotency.set(key, {
|
|
2322
|
-
hash,
|
|
2323
|
-
status: result.status,
|
|
2324
|
-
body: text
|
|
2325
|
-
});
|
|
2326
|
-
}
|
|
2327
|
-
if (req.method === "DELETE" && result.status === 200 && segments[0] === "accounts" && segments.length === 2) {
|
|
2328
|
-
const deletedRef = decode(segments[1]);
|
|
2329
|
-
for (const [stored, entry] of idempotency) if (stored !== key && entry.body.includes(`"ref":"${deletedRef}"`) || entry.body.includes(`"accountRef":"${deletedRef}"`)) idempotency.delete(stored);
|
|
2330
|
-
}
|
|
2331
|
-
return result;
|
|
2332
|
-
}
|
|
2333
|
-
}
|
|
2334
|
-
//#endregion
|
|
2335
|
-
//#region src/testing/portal.ts
|
|
2336
|
-
const PORTAL_DONE = [
|
|
2337
|
-
"canceled",
|
|
2338
|
-
"retry_requested",
|
|
2339
|
-
"profile_saved"
|
|
2340
|
-
];
|
|
2341
|
-
const PORTAL_ERRORS = [
|
|
2342
|
-
"provider_error",
|
|
2343
|
-
"cancel_not_allowed",
|
|
2344
|
-
"retry_not_allowed",
|
|
2345
|
-
"no_subscription",
|
|
2346
|
-
"already_canceled",
|
|
2347
|
-
"nothing_to_retry",
|
|
2348
|
-
"retry_too_soon"
|
|
2349
|
-
];
|
|
2350
|
-
/** Domain nedeni → bildirim kodu (servisin `notAllowedError` eşlemesi). */
|
|
2351
|
-
const REASON_FLASH = {
|
|
2352
|
-
no_subscription: "no_subscription",
|
|
2353
|
-
already_scheduled: "already_canceled",
|
|
2354
|
-
nothing_to_retry: "nothing_to_retry",
|
|
2355
|
-
no_failed_payment: "nothing_to_retry",
|
|
2356
|
-
retry_too_soon: "retry_too_soon"
|
|
2357
|
-
};
|
|
2358
|
-
const DONE_TEXT = {
|
|
2359
|
-
canceled: "Aboneliğiniz dönem sonunda iptal edilecek.",
|
|
2360
|
-
retry_requested: "Ödeme yeniden deneniyor; sonuç birazdan hesabınıza yansır.",
|
|
2361
|
-
profile_saved: "Fatura bilgileriniz kaydedildi."
|
|
2362
|
-
};
|
|
2363
|
-
const ERROR_TEXT = {
|
|
2364
|
-
provider_error: "Ödeme sağlayıcısına ulaşılamadı.",
|
|
2365
|
-
cancel_not_allowed: "Abonelik şu anda iptal edilemiyor.",
|
|
2366
|
-
retry_not_allowed: "Ödeme şu anda yeniden denenemiyor.",
|
|
2367
|
-
no_subscription: "Aktif aboneliğiniz yok.",
|
|
2368
|
-
already_canceled: "Aboneliğiniz zaten dönem sonunda iptal edilecek.",
|
|
2369
|
-
nothing_to_retry: "Yeniden denenecek bir ödeme yok.",
|
|
2370
|
-
retry_too_soon: "Ödeme az önce yeniden denendi; birkaç dakika sonra tekrar deneyin."
|
|
2371
|
-
};
|
|
2372
|
-
const HEADERS = {
|
|
2373
|
-
"cache-control": "no-store",
|
|
2374
|
-
"referrer-policy": "no-referrer"
|
|
2375
|
-
};
|
|
2376
|
-
const htmlResponse = (status, title, body) => new Response(`<!doctype html><html lang="tr"><meta charset="utf-8"><title>${escapeHtml(title)}</title><body>${body}</body></html>`, {
|
|
2377
|
-
status,
|
|
2378
|
-
headers: {
|
|
2379
|
-
...HEADERS,
|
|
2380
|
-
"content-type": "text/html; charset=utf-8"
|
|
2381
|
-
}
|
|
2382
|
-
});
|
|
2383
|
-
const notFound = () => htmlResponse(404, "Sayfa bulunamadı", "<h1>Sayfa bulunamadı</h1><p>Bu portal bağlantısı geçersiz ya da süresi dolmuş.</p>");
|
|
2384
|
-
const redirect = (location) => new Response(null, {
|
|
2385
|
-
status: 303,
|
|
2386
|
-
headers: {
|
|
2387
|
-
...HEADERS,
|
|
2388
|
-
location
|
|
2389
|
-
}
|
|
2390
|
-
});
|
|
2391
|
-
const PATH = /^\/public\/v1\/portal\/([^/]+)(?:\/(cancel|retry-payment|profile)|\/invoices\/([^/]+)\/document)?$/;
|
|
2392
|
-
const day = (ms) => ms === null ? "—" : iso(ms).slice(0, 10);
|
|
2393
|
-
const PROFILE_FIELDS = [
|
|
2394
|
-
"kind",
|
|
2395
|
-
"contactName",
|
|
2396
|
-
"email",
|
|
2397
|
-
"phone",
|
|
2398
|
-
"address",
|
|
2399
|
-
"city",
|
|
2400
|
-
"companyName",
|
|
2401
|
-
"taxId",
|
|
2402
|
-
"taxOffice"
|
|
2403
|
-
];
|
|
2404
|
-
const FIELD_LABELS = {
|
|
2405
|
-
contactName: "Ad soyad",
|
|
2406
|
-
email: "E-posta",
|
|
2407
|
-
phone: "Telefon",
|
|
2408
|
-
address: "Adres",
|
|
2409
|
-
city: "Şehir",
|
|
2410
|
-
companyName: "Şirket unvanı",
|
|
2411
|
-
taxId: "Vergi kimlik numarası",
|
|
2412
|
-
taxOffice: "Vergi dairesi"
|
|
2413
|
-
};
|
|
2414
|
-
/** Servisin `normalizePhone`'u (form.ts): boşluk/parantez/tire atılır, TR yazımları E.164'e. */
|
|
2415
|
-
function normalizePhone(input) {
|
|
2416
|
-
const compact = input.replace(/[\s().-]/g, "");
|
|
2417
|
-
if (/^\+\d+$/.test(compact)) return compact;
|
|
2418
|
-
if (/^00\d+$/.test(compact)) return `+${compact.slice(2)}`;
|
|
2419
|
-
if (/^0\d{10}$/.test(compact)) return `+90${compact.slice(1)}`;
|
|
2420
|
-
if (/^5\d{9}$/.test(compact)) return `+90${compact}`;
|
|
2421
|
-
if (/^90\d{10}$/.test(compact)) return `+${compact}`;
|
|
2422
|
-
return compact;
|
|
2423
|
-
}
|
|
2424
|
-
/**
|
|
2425
|
-
* Servisin `buildProfile`'ı (portal-form.ts): sayfa yalnızca türü ve serbest metinde `<`/`>`
|
|
2426
|
-
* denetler (bu hatalar domain'e gitmez); değerler kırpılır, telefon normalleşir, VKN'deki
|
|
2427
|
-
* boşluklar atılır. Asıl doğrulama `ops.updatePortalProfile`'da.
|
|
2428
|
-
*/
|
|
2429
|
-
function buildProfile(values) {
|
|
2430
|
-
const fieldErrors = {};
|
|
2431
|
-
const kind = values.kind === "individual" || values.kind === "company" ? values.kind : null;
|
|
2432
|
-
if (!kind) fieldErrors["profile.kind"] = "invalid_kind";
|
|
2433
|
-
const textFields = kind === "company" ? [
|
|
2434
|
-
"contactName",
|
|
2435
|
-
"address",
|
|
2436
|
-
"city",
|
|
2437
|
-
"companyName",
|
|
2438
|
-
"taxOffice"
|
|
2439
|
-
] : [
|
|
2440
|
-
"contactName",
|
|
2441
|
-
"address",
|
|
2442
|
-
"city"
|
|
2443
|
-
];
|
|
2444
|
-
for (const field of textFields) if (/[<>]/.test(values[field])) fieldErrors[`profile.${field}`] = "invalid_characters";
|
|
2445
|
-
if (!kind || Object.keys(fieldErrors).length > 0) return {
|
|
2446
|
-
ok: false,
|
|
2447
|
-
fieldErrors
|
|
2448
|
-
};
|
|
2449
|
-
return {
|
|
2450
|
-
ok: true,
|
|
2451
|
-
profile: {
|
|
2452
|
-
kind,
|
|
2453
|
-
contactName: values.contactName.trim(),
|
|
2454
|
-
email: values.email.trim(),
|
|
2455
|
-
phone: normalizePhone(values.phone),
|
|
2456
|
-
address: values.address.trim(),
|
|
2457
|
-
city: values.city.trim(),
|
|
2458
|
-
...kind === "company" ? {
|
|
2459
|
-
companyName: values.companyName.trim(),
|
|
2460
|
-
taxId: values.taxId.replace(/\s/g, ""),
|
|
2461
|
-
taxOffice: values.taxOffice.trim()
|
|
2462
|
-
} : {}
|
|
2463
|
-
}
|
|
2464
|
-
};
|
|
2465
|
-
}
|
|
2466
|
-
function readProfileValues(form) {
|
|
2467
|
-
return Object.fromEntries(PROFILE_FIELDS.map((field) => [field, (form.get(field) ?? "").slice(0, 1e3)]));
|
|
2468
|
-
}
|
|
2469
|
-
function storedValues(view) {
|
|
2470
|
-
const p = view.account.profile;
|
|
2471
|
-
return {
|
|
2472
|
-
kind: p?.kind ?? "individual",
|
|
2473
|
-
contactName: p?.contactName ?? "",
|
|
2474
|
-
email: p?.email ?? "",
|
|
2475
|
-
phone: p?.phone ?? "",
|
|
2476
|
-
address: p?.address ?? "",
|
|
2477
|
-
city: p?.city ?? "",
|
|
2478
|
-
companyName: p?.companyName ?? "",
|
|
2479
|
-
taxId: p?.taxId ?? "",
|
|
2480
|
-
taxOffice: p?.taxOffice ?? ""
|
|
2481
|
-
};
|
|
2482
|
-
}
|
|
2483
|
-
/** `/public/v1/portal/*` istekleri (kimliksiz). Eşleşmeyen yol ve yöntem 404. */
|
|
2484
|
-
function handlePortal(w, req, options) {
|
|
2485
|
-
const match = PATH.exec(req.url.pathname);
|
|
2486
|
-
if (!match) return notFound();
|
|
2487
|
-
let id;
|
|
2488
|
-
let invoiceId;
|
|
2489
|
-
try {
|
|
2490
|
-
id = decodeURIComponent(match[1]);
|
|
2491
|
-
invoiceId = match[3] === void 0 ? void 0 : decodeURIComponent(match[3]);
|
|
2492
|
-
} catch {
|
|
2493
|
-
return notFound();
|
|
2494
|
-
}
|
|
2495
|
-
const action = match[2];
|
|
2496
|
-
const path = `${options.baseUrl}/public/v1/portal/${encodeURIComponent(id)}`;
|
|
2497
|
-
const pageUrl = (k, flash = {}) => `${path}?k=${encodeURIComponent(k)}${flash.done ? `&done=${flash.done}` : flash.error ? `&error=${flash.error}` : ""}`;
|
|
2498
|
-
if (action === void 0) {
|
|
2499
|
-
if (req.method !== "GET") return notFound();
|
|
2500
|
-
const k = req.url.searchParams.get("k") ?? "";
|
|
2501
|
-
const session = openPortalSession(w, id, k);
|
|
2502
|
-
if (!session) return notFound();
|
|
2503
|
-
if (invoiceId !== void 0) {
|
|
2504
|
-
const document = portalInvoiceDocument(w, session, invoiceId);
|
|
2505
|
-
if (!document) return notFound();
|
|
2506
|
-
return new Response(new Uint8Array(document.bytes), {
|
|
2507
|
-
status: 200,
|
|
2508
|
-
headers: {
|
|
2509
|
-
...HEADERS,
|
|
2510
|
-
"content-type": "application/pdf",
|
|
2511
|
-
"content-disposition": `attachment; filename="${document.filename}"`,
|
|
2512
|
-
"content-length": String(document.bytes.byteLength),
|
|
2513
|
-
"x-content-type-options": "nosniff"
|
|
2514
|
-
}
|
|
2515
|
-
});
|
|
2516
|
-
}
|
|
2517
|
-
const doneParam = req.url.searchParams.get("done") ?? void 0;
|
|
2518
|
-
const done = PORTAL_DONE.includes(doneParam ?? "") ? doneParam : null;
|
|
2519
|
-
const errorParam = req.url.searchParams.get("error") ?? void 0;
|
|
2520
|
-
const error = !done && PORTAL_ERRORS.includes(errorParam ?? "") ? errorParam : null;
|
|
2521
|
-
const view = portalView(w, session);
|
|
2522
|
-
return portalPage(view, path, k, {
|
|
2523
|
-
done,
|
|
2524
|
-
error,
|
|
2525
|
-
values: storedValues(view),
|
|
2526
|
-
fieldErrors: {}
|
|
2527
|
-
}, 200);
|
|
2528
|
-
}
|
|
2529
|
-
if (req.method !== "POST") return notFound();
|
|
2530
|
-
const form = new URLSearchParams(req.body);
|
|
2531
|
-
const k = (form.get("k") ?? "").slice(0, 1e3);
|
|
2532
|
-
const session = openPortalSession(w, id, k);
|
|
2533
|
-
if (!session) return notFound();
|
|
2534
|
-
switch (action) {
|
|
2535
|
-
case "cancel": {
|
|
2536
|
-
if (form.get("confirm") !== "1") {
|
|
2537
|
-
const sub = liveSubscription(w, session.accountRef);
|
|
2538
|
-
if (!sub) return redirect(pageUrl(k, { error: "no_subscription" }));
|
|
2539
|
-
if (sub.cancelAtPeriodEnd) return redirect(pageUrl(k, { error: "already_canceled" }));
|
|
2540
|
-
return confirmPage(portalView(w, session), path, k);
|
|
2541
|
-
}
|
|
2542
|
-
const result = portalCancel(w, session);
|
|
2543
|
-
return redirect(pageUrl(k, result === "ok" ? { done: "canceled" } : { error: REASON_FLASH[result] }));
|
|
2544
|
-
}
|
|
2545
|
-
case "retry-payment": {
|
|
2546
|
-
const result = portalRetryPayment(w, session);
|
|
2547
|
-
return redirect(pageUrl(k, result === "ok" ? { done: "retry_requested" } : { error: REASON_FLASH[result] }));
|
|
2548
|
-
}
|
|
2549
|
-
case "profile": {
|
|
2550
|
-
const values = readProfileValues(form);
|
|
2551
|
-
const built = buildProfile(values);
|
|
2552
|
-
const result = built.ok ? updatePortalProfile(w, session, built.profile) : built;
|
|
2553
|
-
if (result.ok) return redirect(pageUrl(k, { done: "profile_saved" }));
|
|
2554
|
-
return portalPage(portalView(w, session), path, k, {
|
|
2555
|
-
done: null,
|
|
2556
|
-
error: null,
|
|
2557
|
-
values,
|
|
2558
|
-
fieldErrors: result.fieldErrors
|
|
2559
|
-
}, 422);
|
|
2560
|
-
}
|
|
2561
|
-
default: return notFound();
|
|
2562
|
-
}
|
|
2563
|
-
}
|
|
2564
|
-
function portalPage(view, path, k, state, status) {
|
|
2565
|
-
const key = `<input type="hidden" name="k" value="${escapeHtml(k)}">`;
|
|
2566
|
-
const { state: account, subscription: sub } = view;
|
|
2567
|
-
const flash = state.done ? `<p role="status" data-done="${state.done}">${escapeHtml(DONE_TEXT[state.done])}</p>` : state.error ? `<p role="alert" data-error="${state.error}">${escapeHtml(ERROR_TEXT[state.error])}</p>` : "";
|
|
2568
|
-
const subscription = sub ? `<p>Abonelik: <strong>${escapeHtml(sub.planName)}</strong> (${escapeHtml(sub.price.planCode)}) · ${escapeHtml(sub.price.interval)} · ${sub.price.amountMinor} ${escapeHtml(sub.price.currency)} (kuruş) · durum: ${escapeHtml(sub.row.status)}</p>
|
|
2569
|
-
<p>Dönem: ${day(sub.row.currentPeriodStart)} – ${day(sub.row.currentPeriodEnd)}</p>
|
|
2570
|
-
${sub.row.cancelAtPeriodEnd ? `<p>Dönem sonunda iptal edilecek: ${day(sub.row.cancelAt ?? sub.row.currentPeriodEnd)}</p>` : ""}
|
|
2571
|
-
${sub.canRetryPayment ? `<form method="post" action="${escapeHtml(path)}/retry-payment">${key}<button>Ödemeyi yeniden dene</button></form>` : ""}
|
|
2572
|
-
${sub.canCancel ? `<form method="post" action="${escapeHtml(path)}/cancel">${key}<button>Dönem sonunda iptal</button></form>` : ""}` : "<p>Aktif aboneliğiniz yok.</p>";
|
|
2573
|
-
const invoices = view.invoices.length ? `<ul>${view.invoices.map((invoice) => {
|
|
2574
|
-
const link = invoice.status === "issued" && invoice.document ? ` <a href="${escapeHtml(`${path}/invoices/${encodeURIComponent(invoice.id)}/document?k=${encodeURIComponent(k)}`)}" download>İndir</a>` : "";
|
|
2575
|
-
return `<li>${escapeHtml(invoice.number ?? "(numarasız)")} · ${day(invoice.issuedAt ?? invoice.createdAt)} · ${invoice.totalMinor} ${escapeHtml(invoice.currency)} · ${escapeHtml(invoice.status)}${link}</li>`;
|
|
2576
|
-
}).join("")}</ul>` : "<p>Fatura yok.</p>";
|
|
2577
|
-
const errors = Object.entries(state.fieldErrors);
|
|
2578
|
-
const fieldError = (field) => state.fieldErrors[`profile.${field}`] ? ` <small data-field-error="profile.${field}">${escapeHtml(state.fieldErrors[`profile.${field}`])}</small>` : "";
|
|
2579
|
-
const input = (field) => `<label>${FIELD_LABELS[field]} <input name="${field}" value="${escapeHtml(state.values[field])}"></label>${fieldError(field)}<br>`;
|
|
2580
|
-
const radio = (value, label) => `<label><input type="radio" name="kind" value="${value}"${state.values.kind === value ? " checked" : ""}> ${label}</label>`;
|
|
2581
|
-
const profile = `<form method="post" action="${escapeHtml(path)}/profile">${key}
|
|
2582
|
-
${errors.length ? `<ul role="alert">${errors.map(([field, code]) => `<li data-field="${escapeHtml(field)}">${escapeHtml(field)}: ${escapeHtml(code)}</li>`).join("")}</ul>` : ""}
|
|
2583
|
-
<p>${radio("individual", "Bireysel")} ${radio("company", "Kurumsal")}${fieldError("kind")}</p>
|
|
2584
|
-
${[
|
|
2585
|
-
"contactName",
|
|
2586
|
-
"email",
|
|
2587
|
-
"phone",
|
|
2588
|
-
"address",
|
|
2589
|
-
"city",
|
|
2590
|
-
"companyName",
|
|
2591
|
-
"taxId",
|
|
2592
|
-
"taxOffice"
|
|
2593
|
-
].map(input).join("\n")}
|
|
2594
|
-
<button>Kaydet</button></form>`;
|
|
2595
|
-
const back = view.returnUrl && /^https?:\/\//.test(view.returnUrl) ? `<p><a href="${escapeHtml(view.returnUrl)}" rel="noreferrer">Ürüne dön</a></p>` : "";
|
|
2596
|
-
return htmlResponse(status, "Sahte steward müşteri portalı", `${back}<h1>Sahte steward müşteri portalı</h1>
|
|
2597
|
-
${flash}
|
|
2598
|
-
<p>Hesap: ${escapeHtml(account.ref)} · ${escapeHtml(account.displayName)} · dil: ${escapeHtml(view.session.locale)}</p>
|
|
2599
|
-
<p>Plan: <strong>${escapeHtml(view.planName)}</strong> (${escapeHtml(account.planCode)}) · erişim: ${escapeHtml(account.accessState)}</p>
|
|
2600
|
-
<h2>Abonelik</h2>
|
|
2601
|
-
${subscription}
|
|
2602
|
-
<h2>Faturalar</h2>
|
|
2603
|
-
${invoices}
|
|
2604
|
-
<h2>Fatura bilgileri</h2>
|
|
2605
|
-
${profile}
|
|
2606
|
-
<p><small>Bağlantı ${iso(view.session.expiresAt)} tarihine kadar geçerli.</small></p>`);
|
|
2607
|
-
}
|
|
2608
|
-
/** İptalin ikinci adımı: `confirm=1` ile aynı rotaya gönderir. */
|
|
2609
|
-
function confirmPage(view, path, k) {
|
|
2610
|
-
const sub = view.subscription;
|
|
2611
|
-
return htmlResponse(200, "Aboneliği iptal et", `<h1>Aboneliği iptal et</h1>
|
|
2612
|
-
<p>${escapeHtml(sub?.planName ?? view.planName)} aboneliğiniz dönem sonunda (${day(sub?.row.currentPeriodEnd ?? null)}) sona erecek; o zamana kadar erişiminiz sürer.</p>
|
|
2613
|
-
<form method="post" action="${escapeHtml(path)}/cancel"><input type="hidden" name="k" value="${escapeHtml(k)}"><input type="hidden" name="confirm" value="1"><button>İptali onayla</button></form>
|
|
2614
|
-
<p><a href="${escapeHtml(`${path}?k=${encodeURIComponent(k)}`)}">Vazgeç</a></p>`);
|
|
2615
|
-
}
|
|
2616
|
-
//#endregion
|
|
2617
|
-
//#region src/testing/signed-event.ts
|
|
2618
|
-
const FIXTURES = {
|
|
2619
|
-
"account.updated": {
|
|
2620
|
-
id: "evt_0192f1a0-0000-7000-8000-000000000010",
|
|
2621
|
-
type: "account.updated",
|
|
2622
|
-
apiVersion: "2026-10-01",
|
|
2623
|
-
createdAt: "2026-10-01T09:00:00Z",
|
|
2624
|
-
product: "example",
|
|
2625
|
-
accountRef: "acct_42",
|
|
2626
|
-
data: {
|
|
2627
|
-
"account": {
|
|
2628
|
-
"version": 3,
|
|
2629
|
-
"planCode": "team",
|
|
2630
|
-
"accessState": "active",
|
|
2631
|
-
"entitlements": {
|
|
2632
|
-
"max_users": null,
|
|
2633
|
-
"max_devices": 250,
|
|
2634
|
-
"max_networks_per_account": 3,
|
|
2635
|
-
"max_approved_routes": 10,
|
|
2636
|
-
"max_exit_nodes": 1
|
|
2637
|
-
},
|
|
2638
|
-
"subscription": {
|
|
2639
|
-
"id": "0192f1a0-0000-7000-8000-000000000001",
|
|
2640
|
-
"status": "active",
|
|
2641
|
-
"planCode": "team",
|
|
2642
|
-
"interval": "month",
|
|
2643
|
-
"amountMinor": 125e3,
|
|
2644
|
-
"currency": "TRY",
|
|
2645
|
-
"currentPeriodEnd": "2026-11-01T09:00:00Z",
|
|
2646
|
-
"cancelAt": null
|
|
2647
|
-
}
|
|
2648
|
-
},
|
|
2649
|
-
"detail": { "changed": ["entitlements"] }
|
|
2650
|
-
}
|
|
2651
|
-
},
|
|
2652
|
-
"checkout.completed": {
|
|
2653
|
-
id: "evt_0192f1a0-0000-7000-8000-000000000011",
|
|
2654
|
-
type: "checkout.completed",
|
|
2655
|
-
apiVersion: "2026-10-01",
|
|
2656
|
-
createdAt: "2026-10-01T09:00:00Z",
|
|
2657
|
-
product: "example",
|
|
2658
|
-
accountRef: "acct_42",
|
|
2659
|
-
data: {
|
|
2660
|
-
"account": {
|
|
2661
|
-
"version": 1,
|
|
2662
|
-
"planCode": "team",
|
|
2663
|
-
"accessState": "active",
|
|
2664
|
-
"entitlements": {
|
|
2665
|
-
"max_users": null,
|
|
2666
|
-
"max_devices": 250,
|
|
2667
|
-
"max_networks_per_account": 3,
|
|
2668
|
-
"max_approved_routes": 10,
|
|
2669
|
-
"max_exit_nodes": 1
|
|
2670
|
-
},
|
|
2671
|
-
"subscription": {
|
|
2672
|
-
"id": "0192f1a0-0000-7000-8000-000000000001",
|
|
2673
|
-
"status": "active",
|
|
2674
|
-
"planCode": "team",
|
|
2675
|
-
"interval": "month",
|
|
2676
|
-
"amountMinor": 125e3,
|
|
2677
|
-
"currency": "TRY",
|
|
2678
|
-
"currentPeriodEnd": "2026-11-01T09:00:00Z",
|
|
2679
|
-
"cancelAt": null
|
|
2680
|
-
}
|
|
2681
|
-
},
|
|
2682
|
-
"detail": {
|
|
2683
|
-
"checkoutSessionId": "0192f1a0-0000-7000-8000-000000000004",
|
|
2684
|
-
"subscriptionId": "0192f1a0-0000-7000-8000-000000000001"
|
|
2685
|
-
}
|
|
2686
|
-
}
|
|
2687
|
-
},
|
|
2688
|
-
"checkout.failed": {
|
|
2689
|
-
id: "evt_0192f1a0-0000-7000-8000-000000000012",
|
|
2690
|
-
type: "checkout.failed",
|
|
2691
|
-
apiVersion: "2026-10-01",
|
|
2692
|
-
createdAt: "2026-10-01T09:00:00Z",
|
|
2693
|
-
product: "example",
|
|
2694
|
-
accountRef: "acct_42",
|
|
2695
|
-
data: {
|
|
2696
|
-
"account": {
|
|
2697
|
-
"version": 0,
|
|
2698
|
-
"planCode": "free",
|
|
2699
|
-
"accessState": "active",
|
|
2700
|
-
"entitlements": {
|
|
2701
|
-
"max_users": 3,
|
|
2702
|
-
"max_devices": 15,
|
|
2703
|
-
"max_networks_per_account": 1,
|
|
2704
|
-
"max_approved_routes": 1,
|
|
2705
|
-
"max_exit_nodes": 0
|
|
2706
|
-
},
|
|
2707
|
-
"subscription": null
|
|
2708
|
-
},
|
|
2709
|
-
"detail": {
|
|
2710
|
-
"checkoutSessionId": "0192f1a0-0000-7000-8000-000000000004",
|
|
2711
|
-
"reason": "provider:card_declined"
|
|
2712
|
-
}
|
|
2713
|
-
}
|
|
2714
|
-
},
|
|
2715
|
-
"checkout.expired": {
|
|
2716
|
-
id: "evt_0192f1a0-0000-7000-8000-000000000013",
|
|
2717
|
-
type: "checkout.expired",
|
|
2718
|
-
apiVersion: "2026-10-01",
|
|
2719
|
-
createdAt: "2026-10-01T09:00:00Z",
|
|
2720
|
-
product: "example",
|
|
2721
|
-
accountRef: "acct_42",
|
|
2722
|
-
data: {
|
|
2723
|
-
"account": {
|
|
2724
|
-
"version": 0,
|
|
2725
|
-
"planCode": "free",
|
|
2726
|
-
"accessState": "active",
|
|
2727
|
-
"entitlements": {
|
|
2728
|
-
"max_users": 3,
|
|
2729
|
-
"max_devices": 15,
|
|
2730
|
-
"max_networks_per_account": 1,
|
|
2731
|
-
"max_approved_routes": 1,
|
|
2732
|
-
"max_exit_nodes": 0
|
|
2733
|
-
},
|
|
2734
|
-
"subscription": null
|
|
2735
|
-
},
|
|
2736
|
-
"detail": { "checkoutSessionId": "0192f1a0-0000-7000-8000-000000000004" }
|
|
2737
|
-
}
|
|
2738
|
-
},
|
|
2739
|
-
"subscription.activated": {
|
|
2740
|
-
id: "evt_0192f1a0-0000-7000-8000-000000000014",
|
|
2741
|
-
type: "subscription.activated",
|
|
2742
|
-
apiVersion: "2026-10-01",
|
|
2743
|
-
createdAt: "2026-10-01T09:00:00Z",
|
|
2744
|
-
product: "example",
|
|
2745
|
-
accountRef: "acct_42",
|
|
2746
|
-
data: {
|
|
2747
|
-
"account": {
|
|
2748
|
-
"version": 1,
|
|
2749
|
-
"planCode": "team",
|
|
2750
|
-
"accessState": "active",
|
|
2751
|
-
"entitlements": {
|
|
2752
|
-
"max_users": null,
|
|
2753
|
-
"max_devices": 250,
|
|
2754
|
-
"max_networks_per_account": 3,
|
|
2755
|
-
"max_approved_routes": 10,
|
|
2756
|
-
"max_exit_nodes": 1
|
|
2757
|
-
},
|
|
2758
|
-
"subscription": {
|
|
2759
|
-
"id": "0192f1a0-0000-7000-8000-000000000001",
|
|
2760
|
-
"status": "active",
|
|
2761
|
-
"planCode": "team",
|
|
2762
|
-
"interval": "month",
|
|
2763
|
-
"amountMinor": 125e3,
|
|
2764
|
-
"currency": "TRY",
|
|
2765
|
-
"currentPeriodEnd": "2026-11-01T09:00:00Z",
|
|
2766
|
-
"cancelAt": null
|
|
2767
|
-
}
|
|
2768
|
-
},
|
|
2769
|
-
"detail": {
|
|
2770
|
-
"subscriptionId": "0192f1a0-0000-7000-8000-000000000001",
|
|
2771
|
-
"planCode": "team",
|
|
2772
|
-
"interval": "month",
|
|
2773
|
-
"amountMinor": 125e3,
|
|
2774
|
-
"currency": "TRY",
|
|
2775
|
-
"actorRef": "user:42"
|
|
2776
|
-
}
|
|
2777
|
-
}
|
|
2778
|
-
},
|
|
2779
|
-
"subscription.renewed": {
|
|
2780
|
-
id: "evt_0192f1a0-0000-7000-8000-000000000015",
|
|
2781
|
-
type: "subscription.renewed",
|
|
2782
|
-
apiVersion: "2026-10-01",
|
|
2783
|
-
createdAt: "2026-10-01T09:00:00Z",
|
|
2784
|
-
product: "example",
|
|
2785
|
-
accountRef: "acct_42",
|
|
2786
|
-
data: {
|
|
2787
|
-
"account": {
|
|
2788
|
-
"version": 2,
|
|
2789
|
-
"planCode": "team",
|
|
2790
|
-
"accessState": "active",
|
|
2791
|
-
"entitlements": {
|
|
2792
|
-
"max_users": null,
|
|
2793
|
-
"max_devices": 250,
|
|
2794
|
-
"max_networks_per_account": 3,
|
|
2795
|
-
"max_approved_routes": 10,
|
|
2796
|
-
"max_exit_nodes": 1
|
|
2797
|
-
},
|
|
2798
|
-
"subscription": {
|
|
2799
|
-
"id": "0192f1a0-0000-7000-8000-000000000001",
|
|
2800
|
-
"status": "active",
|
|
2801
|
-
"planCode": "team",
|
|
2802
|
-
"interval": "month",
|
|
2803
|
-
"amountMinor": 125e3,
|
|
2804
|
-
"currency": "TRY",
|
|
2805
|
-
"currentPeriodEnd": "2026-12-01T09:00:00Z",
|
|
2806
|
-
"cancelAt": null
|
|
2807
|
-
}
|
|
2808
|
-
},
|
|
2809
|
-
"detail": {
|
|
2810
|
-
"subscriptionId": "0192f1a0-0000-7000-8000-000000000001",
|
|
2811
|
-
"paymentId": "0192f1a0-0000-7000-8000-000000000002",
|
|
2812
|
-
"periodStart": "2026-11-01T09:00:00Z",
|
|
2813
|
-
"periodEnd": "2026-12-01T09:00:00Z",
|
|
2814
|
-
"invoiceId": "0192f1a0-0000-7000-8000-000000000003"
|
|
2815
|
-
}
|
|
2816
|
-
}
|
|
2817
|
-
},
|
|
2818
|
-
"subscription.payment_failed": {
|
|
2819
|
-
id: "evt_0192f1a0-0000-7000-8000-000000000016",
|
|
2820
|
-
type: "subscription.payment_failed",
|
|
2821
|
-
apiVersion: "2026-10-01",
|
|
2822
|
-
createdAt: "2026-10-01T09:00:00Z",
|
|
2823
|
-
product: "example",
|
|
2824
|
-
accountRef: "acct_42",
|
|
2825
|
-
data: {
|
|
2826
|
-
"account": {
|
|
2827
|
-
"version": 2,
|
|
2828
|
-
"planCode": "team",
|
|
2829
|
-
"accessState": "active",
|
|
2830
|
-
"entitlements": {
|
|
2831
|
-
"max_users": null,
|
|
2832
|
-
"max_devices": 250,
|
|
2833
|
-
"max_networks_per_account": 3,
|
|
2834
|
-
"max_approved_routes": 10,
|
|
2835
|
-
"max_exit_nodes": 1
|
|
2836
|
-
},
|
|
2837
|
-
"subscription": {
|
|
2838
|
-
"id": "0192f1a0-0000-7000-8000-000000000001",
|
|
2839
|
-
"status": "past_due",
|
|
2840
|
-
"planCode": "team",
|
|
2841
|
-
"interval": "month",
|
|
2842
|
-
"amountMinor": 125e3,
|
|
2843
|
-
"currency": "TRY",
|
|
2844
|
-
"currentPeriodEnd": "2026-11-01T09:00:00Z",
|
|
2845
|
-
"cancelAt": null
|
|
2846
|
-
}
|
|
2847
|
-
},
|
|
2848
|
-
"detail": {
|
|
2849
|
-
"subscriptionId": "0192f1a0-0000-7000-8000-000000000001",
|
|
2850
|
-
"paymentId": "0192f1a0-0000-7000-8000-000000000002",
|
|
2851
|
-
"attempt": 1,
|
|
2852
|
-
"nextRetryAt": "2026-11-02T09:00:00Z"
|
|
2853
|
-
}
|
|
2854
|
-
}
|
|
2855
|
-
},
|
|
2856
|
-
"subscription.suspended": {
|
|
2857
|
-
id: "evt_0192f1a0-0000-7000-8000-000000000017",
|
|
2858
|
-
type: "subscription.suspended",
|
|
2859
|
-
apiVersion: "2026-10-01",
|
|
2860
|
-
createdAt: "2026-10-01T09:00:00Z",
|
|
2861
|
-
product: "example",
|
|
2862
|
-
accountRef: "acct_42",
|
|
2863
|
-
data: {
|
|
2864
|
-
"account": {
|
|
2865
|
-
"version": 3,
|
|
2866
|
-
"planCode": "team",
|
|
2867
|
-
"accessState": "suspended",
|
|
2868
|
-
"entitlements": {
|
|
2869
|
-
"max_users": null,
|
|
2870
|
-
"max_devices": 250,
|
|
2871
|
-
"max_networks_per_account": 3,
|
|
2872
|
-
"max_approved_routes": 10,
|
|
2873
|
-
"max_exit_nodes": 1
|
|
2874
|
-
},
|
|
2875
|
-
"subscription": {
|
|
2876
|
-
"id": "0192f1a0-0000-7000-8000-000000000001",
|
|
2877
|
-
"status": "suspended",
|
|
2878
|
-
"planCode": "team",
|
|
2879
|
-
"interval": "month",
|
|
2880
|
-
"amountMinor": 125e3,
|
|
2881
|
-
"currency": "TRY",
|
|
2882
|
-
"currentPeriodEnd": "2026-11-01T09:00:00Z",
|
|
2883
|
-
"cancelAt": null
|
|
2884
|
-
}
|
|
2885
|
-
},
|
|
2886
|
-
"detail": {
|
|
2887
|
-
"subscriptionId": "0192f1a0-0000-7000-8000-000000000001",
|
|
2888
|
-
"dunningDay": 7
|
|
2889
|
-
}
|
|
2890
|
-
}
|
|
2891
|
-
},
|
|
2892
|
-
"subscription.reactivated": {
|
|
2893
|
-
id: "evt_0192f1a0-0000-7000-8000-000000000018",
|
|
2894
|
-
type: "subscription.reactivated",
|
|
2895
|
-
apiVersion: "2026-10-01",
|
|
2896
|
-
createdAt: "2026-10-01T09:00:00Z",
|
|
2897
|
-
product: "example",
|
|
2898
|
-
accountRef: "acct_42",
|
|
2899
|
-
data: {
|
|
2900
|
-
"account": {
|
|
2901
|
-
"version": 4,
|
|
2902
|
-
"planCode": "team",
|
|
2903
|
-
"accessState": "active",
|
|
2904
|
-
"entitlements": {
|
|
2905
|
-
"max_users": null,
|
|
2906
|
-
"max_devices": 250,
|
|
2907
|
-
"max_networks_per_account": 3,
|
|
2908
|
-
"max_approved_routes": 10,
|
|
2909
|
-
"max_exit_nodes": 1
|
|
2910
|
-
},
|
|
2911
|
-
"subscription": {
|
|
2912
|
-
"id": "0192f1a0-0000-7000-8000-000000000001",
|
|
2913
|
-
"status": "active",
|
|
2914
|
-
"planCode": "team",
|
|
2915
|
-
"interval": "month",
|
|
2916
|
-
"amountMinor": 125e3,
|
|
2917
|
-
"currency": "TRY",
|
|
2918
|
-
"currentPeriodEnd": "2026-11-01T09:00:00Z",
|
|
2919
|
-
"cancelAt": null
|
|
2920
|
-
}
|
|
2921
|
-
},
|
|
2922
|
-
"detail": {
|
|
2923
|
-
"subscriptionId": "0192f1a0-0000-7000-8000-000000000001",
|
|
2924
|
-
"paymentId": "0192f1a0-0000-7000-8000-000000000002"
|
|
2925
|
-
}
|
|
2926
|
-
}
|
|
2927
|
-
},
|
|
2928
|
-
"subscription.cancel_scheduled": {
|
|
2929
|
-
id: "evt_0192f1a0-0000-7000-8000-000000000019",
|
|
2930
|
-
type: "subscription.cancel_scheduled",
|
|
2931
|
-
apiVersion: "2026-10-01",
|
|
2932
|
-
createdAt: "2026-10-01T09:00:00Z",
|
|
2933
|
-
product: "example",
|
|
2934
|
-
accountRef: "acct_42",
|
|
2935
|
-
data: {
|
|
2936
|
-
"account": {
|
|
2937
|
-
"version": 2,
|
|
2938
|
-
"planCode": "team",
|
|
2939
|
-
"accessState": "active",
|
|
2940
|
-
"entitlements": {
|
|
2941
|
-
"max_users": null,
|
|
2942
|
-
"max_devices": 250,
|
|
2943
|
-
"max_networks_per_account": 3,
|
|
2944
|
-
"max_approved_routes": 10,
|
|
2945
|
-
"max_exit_nodes": 1
|
|
2946
|
-
},
|
|
2947
|
-
"subscription": {
|
|
2948
|
-
"id": "0192f1a0-0000-7000-8000-000000000001",
|
|
2949
|
-
"status": "active",
|
|
2950
|
-
"planCode": "team",
|
|
2951
|
-
"interval": "month",
|
|
2952
|
-
"amountMinor": 125e3,
|
|
2953
|
-
"currency": "TRY",
|
|
2954
|
-
"currentPeriodEnd": "2026-11-01T09:00:00Z",
|
|
2955
|
-
"cancelAt": "2026-11-01T09:00:00Z"
|
|
2956
|
-
}
|
|
2957
|
-
},
|
|
2958
|
-
"detail": {
|
|
2959
|
-
"subscriptionId": "0192f1a0-0000-7000-8000-000000000001",
|
|
2960
|
-
"cancelAt": "2026-11-01T09:00:00Z",
|
|
2961
|
-
"reason": "müşteri talebi",
|
|
2962
|
-
"actorRef": "staff:7"
|
|
2963
|
-
}
|
|
2964
|
-
}
|
|
2965
|
-
},
|
|
2966
|
-
"subscription.canceled": {
|
|
2967
|
-
id: "evt_0192f1a0-0000-7000-8000-00000000001a",
|
|
2968
|
-
type: "subscription.canceled",
|
|
2969
|
-
apiVersion: "2026-10-01",
|
|
2970
|
-
createdAt: "2026-10-01T09:00:00Z",
|
|
2971
|
-
product: "example",
|
|
2972
|
-
accountRef: "acct_42",
|
|
2973
|
-
data: {
|
|
2974
|
-
"account": {
|
|
2975
|
-
"version": 2,
|
|
2976
|
-
"planCode": "free",
|
|
2977
|
-
"accessState": "active",
|
|
2978
|
-
"entitlements": {
|
|
2979
|
-
"max_users": 3,
|
|
2980
|
-
"max_devices": 15,
|
|
2981
|
-
"max_networks_per_account": 1,
|
|
2982
|
-
"max_approved_routes": 1,
|
|
2983
|
-
"max_exit_nodes": 0
|
|
2984
|
-
},
|
|
2985
|
-
"subscription": null
|
|
2986
|
-
},
|
|
2987
|
-
"detail": {
|
|
2988
|
-
"subscriptionId": "0192f1a0-0000-7000-8000-000000000001",
|
|
2989
|
-
"reason": "müşteri talebi",
|
|
2990
|
-
"effectiveAt": "2026-10-15T12:00:00Z",
|
|
2991
|
-
"actorRef": "staff:7"
|
|
2992
|
-
}
|
|
2993
|
-
}
|
|
2994
|
-
},
|
|
2995
|
-
"subscription.expired": {
|
|
2996
|
-
id: "evt_0192f1a0-0000-7000-8000-00000000001b",
|
|
2997
|
-
type: "subscription.expired",
|
|
2998
|
-
apiVersion: "2026-10-01",
|
|
2999
|
-
createdAt: "2026-10-01T09:00:00Z",
|
|
3000
|
-
product: "example",
|
|
3001
|
-
accountRef: "acct_42",
|
|
3002
|
-
data: {
|
|
3003
|
-
"account": {
|
|
3004
|
-
"version": 3,
|
|
3005
|
-
"planCode": "free",
|
|
3006
|
-
"accessState": "active",
|
|
3007
|
-
"entitlements": {
|
|
3008
|
-
"max_users": 3,
|
|
3009
|
-
"max_devices": 15,
|
|
3010
|
-
"max_networks_per_account": 1,
|
|
3011
|
-
"max_approved_routes": 1,
|
|
3012
|
-
"max_exit_nodes": 0
|
|
3013
|
-
},
|
|
3014
|
-
"subscription": null
|
|
3015
|
-
},
|
|
3016
|
-
"detail": {
|
|
3017
|
-
"subscriptionId": "0192f1a0-0000-7000-8000-000000000001",
|
|
3018
|
-
"effectiveAt": "2026-11-01T09:00:00Z"
|
|
3019
|
-
}
|
|
3020
|
-
}
|
|
3021
|
-
},
|
|
3022
|
-
"subscription.terminated": {
|
|
3023
|
-
id: "evt_0192f1a0-0000-7000-8000-00000000001c",
|
|
3024
|
-
type: "subscription.terminated",
|
|
3025
|
-
apiVersion: "2026-10-01",
|
|
3026
|
-
createdAt: "2026-10-01T09:00:00Z",
|
|
3027
|
-
product: "example",
|
|
3028
|
-
accountRef: "acct_42",
|
|
3029
|
-
data: {
|
|
3030
|
-
"account": {
|
|
3031
|
-
"version": 5,
|
|
3032
|
-
"planCode": "free",
|
|
3033
|
-
"accessState": "active",
|
|
3034
|
-
"entitlements": {
|
|
3035
|
-
"max_users": 3,
|
|
3036
|
-
"max_devices": 15,
|
|
3037
|
-
"max_networks_per_account": 1,
|
|
3038
|
-
"max_approved_routes": 1,
|
|
3039
|
-
"max_exit_nodes": 0
|
|
3040
|
-
},
|
|
3041
|
-
"subscription": null
|
|
3042
|
-
},
|
|
3043
|
-
"detail": {
|
|
3044
|
-
"subscriptionId": "0192f1a0-0000-7000-8000-000000000001",
|
|
3045
|
-
"reason": "nonpayment",
|
|
3046
|
-
"dunningDay": 30
|
|
3047
|
-
}
|
|
3048
|
-
}
|
|
3049
|
-
},
|
|
3050
|
-
"invoice.created": {
|
|
3051
|
-
id: "evt_0192f1a0-0000-7000-8000-00000000001d",
|
|
3052
|
-
type: "invoice.created",
|
|
3053
|
-
apiVersion: "2026-10-01",
|
|
3054
|
-
createdAt: "2026-10-01T09:00:00Z",
|
|
3055
|
-
product: "example",
|
|
3056
|
-
accountRef: "acct_42",
|
|
3057
|
-
data: {
|
|
3058
|
-
"account": {
|
|
3059
|
-
"version": 2,
|
|
3060
|
-
"planCode": "team",
|
|
3061
|
-
"accessState": "active",
|
|
3062
|
-
"entitlements": {
|
|
3063
|
-
"max_users": null,
|
|
3064
|
-
"max_devices": 250,
|
|
3065
|
-
"max_networks_per_account": 3,
|
|
3066
|
-
"max_approved_routes": 10,
|
|
3067
|
-
"max_exit_nodes": 1
|
|
3068
|
-
},
|
|
3069
|
-
"subscription": {
|
|
3070
|
-
"id": "0192f1a0-0000-7000-8000-000000000001",
|
|
3071
|
-
"status": "active",
|
|
3072
|
-
"planCode": "team",
|
|
3073
|
-
"interval": "month",
|
|
3074
|
-
"amountMinor": 125e3,
|
|
3075
|
-
"currency": "TRY",
|
|
3076
|
-
"currentPeriodEnd": "2026-11-01T09:00:00Z",
|
|
3077
|
-
"cancelAt": null
|
|
3078
|
-
}
|
|
3079
|
-
},
|
|
3080
|
-
"detail": {
|
|
3081
|
-
"invoiceId": "0192f1a0-0000-7000-8000-000000000003",
|
|
3082
|
-
"totalMinor": 125e3,
|
|
3083
|
-
"currency": "TRY"
|
|
3084
|
-
}
|
|
3085
|
-
}
|
|
3086
|
-
},
|
|
3087
|
-
"invoice.issued": {
|
|
3088
|
-
id: "evt_0192f1a0-0000-7000-8000-00000000001e",
|
|
3089
|
-
type: "invoice.issued",
|
|
3090
|
-
apiVersion: "2026-10-01",
|
|
3091
|
-
createdAt: "2026-10-01T09:00:00Z",
|
|
3092
|
-
product: "example",
|
|
3093
|
-
accountRef: "acct_42",
|
|
3094
|
-
data: {
|
|
3095
|
-
"account": {
|
|
3096
|
-
"version": 2,
|
|
3097
|
-
"planCode": "team",
|
|
3098
|
-
"accessState": "active",
|
|
3099
|
-
"entitlements": {
|
|
3100
|
-
"max_users": null,
|
|
3101
|
-
"max_devices": 250,
|
|
3102
|
-
"max_networks_per_account": 3,
|
|
3103
|
-
"max_approved_routes": 10,
|
|
3104
|
-
"max_exit_nodes": 1
|
|
3105
|
-
},
|
|
3106
|
-
"subscription": {
|
|
3107
|
-
"id": "0192f1a0-0000-7000-8000-000000000001",
|
|
3108
|
-
"status": "active",
|
|
3109
|
-
"planCode": "team",
|
|
3110
|
-
"interval": "month",
|
|
3111
|
-
"amountMinor": 125e3,
|
|
3112
|
-
"currency": "TRY",
|
|
3113
|
-
"currentPeriodEnd": "2026-11-01T09:00:00Z",
|
|
3114
|
-
"cancelAt": null
|
|
3115
|
-
}
|
|
3116
|
-
},
|
|
3117
|
-
"detail": {
|
|
3118
|
-
"invoiceId": "0192f1a0-0000-7000-8000-000000000003",
|
|
3119
|
-
"number": "NXS2026000000123",
|
|
3120
|
-
"totalMinor": 125e3,
|
|
3121
|
-
"currency": "TRY"
|
|
3122
|
-
}
|
|
3123
|
-
}
|
|
3124
|
-
},
|
|
3125
|
-
"invoice.voided": {
|
|
3126
|
-
id: "evt_0192f1a0-0000-7000-8000-000000000f4f",
|
|
3127
|
-
type: "invoice.voided",
|
|
3128
|
-
apiVersion: "2026-10-01",
|
|
3129
|
-
createdAt: "2026-10-01T09:00:00Z",
|
|
3130
|
-
product: "example",
|
|
3131
|
-
accountRef: "acct_42",
|
|
3132
|
-
data: {
|
|
3133
|
-
"account": {
|
|
3134
|
-
"ref": "acct_42",
|
|
3135
|
-
"displayName": "Örnek A.Ş.",
|
|
3136
|
-
"version": 4,
|
|
3137
|
-
"updatedAt": "2026-09-20T09:00:00Z",
|
|
3138
|
-
"planCode": "enterprise",
|
|
3139
|
-
"accessState": "active",
|
|
3140
|
-
"entitlements": {
|
|
3141
|
-
"max_users": null,
|
|
3142
|
-
"max_devices": null,
|
|
3143
|
-
"max_networks_per_account": 20,
|
|
3144
|
-
"max_approved_routes": 100,
|
|
3145
|
-
"max_exit_nodes": 10
|
|
3146
|
-
},
|
|
3147
|
-
"catalogVersion": "3f2a9c0e7b41d8a65c09e1f4b7d2a8c3",
|
|
3148
|
-
"subscription": null,
|
|
3149
|
-
"openCheckout": null,
|
|
3150
|
-
"profile": {
|
|
3151
|
-
"present": true,
|
|
3152
|
-
"kind": "company"
|
|
3153
|
-
}
|
|
3154
|
-
},
|
|
3155
|
-
"detail": {
|
|
3156
|
-
"invoiceId": "0192f1a0-0000-7000-8000-000000000004",
|
|
3157
|
-
"number": "NXS2026000000124",
|
|
3158
|
-
"totalMinor": 6e6,
|
|
3159
|
-
"currency": "TRY",
|
|
3160
|
-
"reason": "sözleşme yeniden düzenlendi"
|
|
3161
|
-
}
|
|
3162
|
-
}
|
|
3163
|
-
},
|
|
3164
|
-
"account.state_changed": {
|
|
3165
|
-
id: "evt_0192f1a0-0000-7000-8000-000000000020",
|
|
3166
|
-
type: "account.state_changed",
|
|
3167
|
-
apiVersion: "2026-10-01",
|
|
3168
|
-
createdAt: "2026-10-01T09:00:00Z",
|
|
3169
|
-
product: "example",
|
|
3170
|
-
accountRef: "acct_42",
|
|
3171
|
-
data: {
|
|
3172
|
-
"account": {
|
|
3173
|
-
"ref": "acct_42",
|
|
3174
|
-
"displayName": "Örnek A.Ş.",
|
|
3175
|
-
"version": 2,
|
|
3176
|
-
"updatedAt": "2026-10-01T09:00:00Z",
|
|
3177
|
-
"planCode": "team",
|
|
3178
|
-
"accessState": "active",
|
|
3179
|
-
"entitlements": {
|
|
3180
|
-
"max_users": null,
|
|
3181
|
-
"max_devices": 250,
|
|
3182
|
-
"max_networks_per_account": 3,
|
|
3183
|
-
"max_approved_routes": 10,
|
|
3184
|
-
"max_exit_nodes": 1
|
|
3185
|
-
},
|
|
3186
|
-
"catalogVersion": "3f2a9c0e7b41d8a65c09e1f4b7d2a8c3",
|
|
3187
|
-
"subscription": {
|
|
3188
|
-
"id": "0192f1a0-0000-7000-8000-000000000001",
|
|
3189
|
-
"status": "past_due",
|
|
3190
|
-
"planCode": "team",
|
|
3191
|
-
"interval": "month",
|
|
3192
|
-
"amountMinor": 125e3,
|
|
3193
|
-
"currency": "TRY",
|
|
3194
|
-
"currentPeriodEnd": "2026-10-01T09:00:00Z",
|
|
3195
|
-
"cancelAt": null,
|
|
3196
|
-
"cancelAtPeriodEnd": false,
|
|
3197
|
-
"pastDueSince": "2026-10-01T09:00:00Z",
|
|
3198
|
-
"currentPeriodStart": "2026-09-01T09:00:00Z",
|
|
3199
|
-
"dunning": {
|
|
3200
|
-
"step": 0,
|
|
3201
|
-
"nextActionAt": "2026-10-02T09:00:00Z"
|
|
3202
|
-
},
|
|
3203
|
-
"scheduledChange": null
|
|
3204
|
-
},
|
|
3205
|
-
"openCheckout": null,
|
|
3206
|
-
"profile": {
|
|
3207
|
-
"present": true,
|
|
3208
|
-
"kind": "company"
|
|
3209
|
-
}
|
|
3210
|
-
},
|
|
3211
|
-
"detail": {
|
|
3212
|
-
"cause": "subscription.payment_failed",
|
|
3213
|
-
"causeEventId": "evt_0192f1a0-0000-7000-8000-000000000016"
|
|
3214
|
-
}
|
|
3215
|
-
}
|
|
3216
|
-
},
|
|
3217
|
-
"account.deleted": {
|
|
3218
|
-
id: "evt_0192f1a0-0000-7000-8000-000000000030",
|
|
3219
|
-
type: "account.deleted",
|
|
3220
|
-
apiVersion: "2026-10-01",
|
|
3221
|
-
createdAt: "2026-10-05T09:00:00Z",
|
|
3222
|
-
product: "example",
|
|
3223
|
-
accountRef: "acct_42",
|
|
3224
|
-
data: {
|
|
3225
|
-
"account": {
|
|
3226
|
-
"ref": "acct_42",
|
|
3227
|
-
"displayName": "",
|
|
3228
|
-
"version": 9,
|
|
3229
|
-
"updatedAt": "2026-10-05T09:00:00Z",
|
|
3230
|
-
"planCode": "free",
|
|
3231
|
-
"accessState": "active",
|
|
3232
|
-
"entitlements": {
|
|
3233
|
-
"max_users": 3,
|
|
3234
|
-
"max_devices": 15,
|
|
3235
|
-
"max_networks_per_account": 1,
|
|
3236
|
-
"max_approved_routes": 0,
|
|
3237
|
-
"max_exit_nodes": 0
|
|
3238
|
-
},
|
|
3239
|
-
"catalogVersion": "3f2a9c0e7b41d8a65c09e1f4b7d2a8c3",
|
|
3240
|
-
"subscription": null,
|
|
3241
|
-
"openCheckout": null,
|
|
3242
|
-
"profile": {
|
|
3243
|
-
"present": false,
|
|
3244
|
-
"kind": null
|
|
3245
|
-
}
|
|
3246
|
-
},
|
|
3247
|
-
"detail": {
|
|
3248
|
-
"deletedAt": "2026-10-05T09:00:00Z",
|
|
3249
|
-
"actorRef": "user:7"
|
|
3250
|
-
}
|
|
3251
|
-
}
|
|
3252
|
-
}
|
|
3253
|
-
};
|
|
3254
|
-
/** Tipin fixture'ının derin kopyası. */
|
|
3255
|
-
function eventFixture(type) {
|
|
3256
|
-
if (!EVENT_TYPES.includes(type)) throw new Error(`bilinmeyen event tipi: ${String(type)}`);
|
|
3257
|
-
return structuredClone(FIXTURES[type]);
|
|
3258
|
-
}
|
|
3259
|
-
const DEFAULT_WEBHOOK_URL = "http://product.test/steward/events";
|
|
3260
|
-
const secretsOf = (secret) => typeof secret === "string" ? [secret] : [...secret];
|
|
3261
|
-
/** Olayı steward'ın teslimat biçimiyle imzalar. */
|
|
3262
|
-
function signBody(event, secrets, timestamp = Math.floor(Date.now() / 1e3)) {
|
|
3263
|
-
const body = JSON.stringify(event);
|
|
3264
|
-
const headers = {
|
|
3265
|
-
...signWebhook(secrets, event.id, body, timestamp),
|
|
3266
|
-
"content-type": "application/json",
|
|
3267
|
-
"user-agent": "steward/1"
|
|
3268
|
-
};
|
|
3269
|
-
return {
|
|
3270
|
-
body,
|
|
3271
|
-
headers,
|
|
3272
|
-
request: (url = DEFAULT_WEBHOOK_URL) => new Request(url, {
|
|
3273
|
-
method: "POST",
|
|
3274
|
-
headers,
|
|
3275
|
-
body
|
|
3276
|
-
})
|
|
3277
|
-
};
|
|
3278
|
-
}
|
|
3279
|
-
function signedEvent(type, overrides = {}, options = {}) {
|
|
3280
|
-
const secret = options.secret ?? globalThis.process?.env?.STEWARD_WEBHOOK_SECRET;
|
|
3281
|
-
const secrets = secret === void 0 ? [] : secretsOf(secret).map((s) => s.trim()).filter(Boolean);
|
|
3282
|
-
if (secrets.length === 0) throw new Error("signedEvent: imza sırrı yok (options.secret ya da STEWARD_WEBHOOK_SECRET)");
|
|
3283
|
-
const base = eventFixture(type);
|
|
3284
|
-
const { data, ...envelope } = overrides;
|
|
3285
|
-
const account = {
|
|
3286
|
-
...base.data.account,
|
|
3287
|
-
...data?.account
|
|
3288
|
-
};
|
|
3289
|
-
if (overrides.accountRef !== void 0 && "ref" in base.data.account && data?.account?.ref === void 0) account.ref = overrides.accountRef;
|
|
3290
|
-
const event = {
|
|
3291
|
-
...base,
|
|
3292
|
-
id: `evt_${globalThis.crypto.randomUUID()}`,
|
|
3293
|
-
...envelope,
|
|
3294
|
-
data: {
|
|
3295
|
-
account,
|
|
3296
|
-
detail: {
|
|
3297
|
-
...base.data.detail,
|
|
3298
|
-
...data?.detail
|
|
3299
|
-
}
|
|
3300
|
-
}
|
|
3301
|
-
};
|
|
3302
|
-
return {
|
|
3303
|
-
event,
|
|
3304
|
-
...signBody(event, secrets, options.timestamp)
|
|
3305
|
-
};
|
|
3306
|
-
}
|
|
3307
|
-
//#endregion
|
|
3308
|
-
//#region src/testing/fake-steward.ts
|
|
3309
|
-
const RETRY_SCHEDULE_SECONDS = [
|
|
3310
|
-
30,
|
|
3311
|
-
120,
|
|
3312
|
-
600,
|
|
3313
|
-
1800,
|
|
3314
|
-
3600,
|
|
3315
|
-
10800,
|
|
3316
|
-
21600
|
|
3317
|
-
];
|
|
3318
|
-
/** Yönetim ucunun (`admin.endpoints.create`) yeni endpoint'e varsayılan filtresi. */
|
|
3319
|
-
const DEFAULT_EVENT_TYPES = ["account.state_changed"];
|
|
3320
|
-
function base64url(bytes) {
|
|
3321
|
-
return btoa(String.fromCharCode(...bytes)).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
|
|
3322
|
-
}
|
|
3323
|
-
/** Servisin anahtar biçimi: `stw_<12 hex>_<43 base64url>`. */
|
|
3324
|
-
function generateApiKey() {
|
|
3325
|
-
const bytes = globalThis.crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(38));
|
|
3326
|
-
return `stw_${Array.from(bytes.subarray(0, 6), (b) => b.toString(16).padStart(2, "0")).join("")}_${base64url(bytes.subarray(6))}`;
|
|
3327
|
-
}
|
|
3328
|
-
const networkError = (reason) => new TypeError("fetch failed", { cause: /* @__PURE__ */ new Error(`sahte steward: ${reason}`) });
|
|
3329
|
-
/** `settings` + kısayol seçenekleri (`returnOrigins`, `checkoutTtlMinutes`) → sahtenin ayarları. */
|
|
3330
|
-
function initialSettings(options) {
|
|
3331
|
-
const patch = { ...options.settings };
|
|
3332
|
-
const alias = (option, field) => {
|
|
3333
|
-
if (options[option] === void 0) return;
|
|
3334
|
-
if (patch[field] !== void 0) throw new Error(`createFakeSteward: ${option} ve settings.${field} birlikte verilemez`);
|
|
3335
|
-
patch[field] = options[option];
|
|
3336
|
-
};
|
|
3337
|
-
alias("returnOrigins", "extraReturnOrigins");
|
|
3338
|
-
alias("checkoutTtlMinutes", "checkoutTtlMinutes");
|
|
3339
|
-
const parsed = ProductSettingsPatchSchema.safeParse(patch);
|
|
3340
|
-
if (!parsed.success) throw new Error(`createFakeSteward: ayarlar geçersiz: ${parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}`);
|
|
3341
|
-
return applySettingsPatch(ProductSettingsSchema.parse({ provider: "fake" }), parsed.data);
|
|
3342
|
-
}
|
|
3343
|
-
function createFakeSteward(options = {}) {
|
|
3344
|
-
const base = new URL(options.baseUrl ?? "http://steward.test");
|
|
3345
|
-
const basePath = base.pathname.replace(/\/$/, "");
|
|
3346
|
-
const baseUrl = `${base.origin}${basePath}`;
|
|
3347
|
-
const apiKey = options.apiKey ?? generateApiKey();
|
|
3348
|
-
const adminApiKey = options.adminApiKey ?? generateApiKey();
|
|
3349
|
-
if (apiKey === adminApiKey) throw new Error("createFakeSteward: apiKey ve adminApiKey farklı olmalı");
|
|
3350
|
-
const webhook = options.webhook;
|
|
3351
|
-
const w = createWorld({
|
|
3352
|
-
productCode: options.productCode ?? "example",
|
|
3353
|
-
settings: initialSettings(options),
|
|
3354
|
-
permissiveReturnOrigins: options.returnOrigins === void 0 && options.settings?.appUrl == null && options.settings?.extraReturnOrigins === void 0,
|
|
3355
|
-
dunningPolicy: options.dunningPolicy ?? DEFAULT_DUNNING_POLICY,
|
|
3356
|
-
endpointTypes: webhook ? webhook.eventTypes === "legacy" ? LEGACY_EVENT_TYPES : webhook.eventTypes ?? DEFAULT_EVENT_TYPES : null,
|
|
3357
|
-
now: options.now ?? (() => /* @__PURE__ */ new Date())
|
|
3358
|
-
});
|
|
3359
|
-
if (options.catalog !== void 0) {
|
|
3360
|
-
const input = "toSyncInput" in options.catalog ? options.catalog.toSyncInput() : options.catalog;
|
|
3361
|
-
const parsed = CatalogSyncInputSchema.safeParse(input);
|
|
3362
|
-
if (!parsed.success) throw new Error(`createFakeSteward: katalog geçersiz: ${parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}`);
|
|
3363
|
-
const synced = syncCatalog(w, parsed.data);
|
|
3364
|
-
if (!synced.ok) throw new Error(`createFakeSteward: katalog reddedildi (${synced.code}): ${synced.message ?? ""}`);
|
|
3365
|
-
}
|
|
3366
|
-
const handle = createHttpHandler(w, {
|
|
3367
|
-
baseUrl,
|
|
3368
|
-
keys: /* @__PURE__ */ new Map([[apiKey, {
|
|
3369
|
-
keyName: "fake",
|
|
3370
|
-
scope: "product"
|
|
3371
|
-
}], [adminApiKey, {
|
|
3372
|
-
keyName: "fake-admin",
|
|
3373
|
-
scope: "admin"
|
|
3374
|
-
}]]),
|
|
3375
|
-
requestId: () => `req_${globalThis.crypto.randomUUID()}`,
|
|
3376
|
-
logger: options.logger ?? console
|
|
3377
|
-
});
|
|
3378
|
-
const requests = [];
|
|
3379
|
-
let outage = null;
|
|
3380
|
-
const fakeFetch = (async (input, init) => {
|
|
3381
|
-
const req = new Request(input, init);
|
|
3382
|
-
if (req.signal.aborted) throw req.signal.reason;
|
|
3383
|
-
const url = new URL(req.url);
|
|
3384
|
-
const body = req.method === "GET" || req.method === "HEAD" ? null : await req.text();
|
|
3385
|
-
const inBase = url.origin === base.origin && (url.pathname === basePath || url.pathname.startsWith(`${basePath}/`));
|
|
3386
|
-
const path = `${inBase ? url.pathname.slice(basePath.length) : url.pathname}${url.search}`;
|
|
3387
|
-
const record = {
|
|
3388
|
-
method: req.method,
|
|
3389
|
-
url: req.url,
|
|
3390
|
-
path,
|
|
3391
|
-
headers: Object.fromEntries(req.headers),
|
|
3392
|
-
body,
|
|
3393
|
-
status: 0
|
|
3394
|
-
};
|
|
3395
|
-
requests.push(record);
|
|
3396
|
-
if (!inBase) throw networkError(`bilinmeyen adres ${url.origin}`);
|
|
3397
|
-
if (outage === "network") throw networkError("kesinti");
|
|
3398
|
-
let res;
|
|
3399
|
-
const publicPath = url.pathname.slice(basePath.length);
|
|
3400
|
-
if (outage === null && (publicPath === "/public" || publicPath.startsWith("/public/"))) {
|
|
3401
|
-
const publicReq = {
|
|
3402
|
-
method: req.method,
|
|
3403
|
-
url: new URL(path, base.origin),
|
|
3404
|
-
body: body ?? ""
|
|
3405
|
-
};
|
|
3406
|
-
res = publicPath.startsWith("/public/v1/portal/") ? handlePortal(w, publicReq, { baseUrl }) : handlePublic(w, publicReq);
|
|
3407
|
-
} else if (outage === "503") {
|
|
3408
|
-
const requestId = `req_${globalThis.crypto.randomUUID()}`;
|
|
3409
|
-
const { category, retryable } = ERROR_CODES.database_unavailable;
|
|
3410
|
-
res = Response.json({
|
|
3411
|
-
error: "database_unavailable",
|
|
3412
|
-
category,
|
|
3413
|
-
retryable,
|
|
3414
|
-
requestId
|
|
3415
|
-
}, {
|
|
3416
|
-
status: 503,
|
|
3417
|
-
headers: { [HEADER_REQUEST_ID]: requestId }
|
|
3418
|
-
});
|
|
3419
|
-
} else res = handle({
|
|
3420
|
-
method: req.method,
|
|
3421
|
-
url: new URL(path, base.origin),
|
|
3422
|
-
headers: req.headers,
|
|
3423
|
-
body: body ?? ""
|
|
3424
|
-
});
|
|
3425
|
-
record.status = res.status;
|
|
3426
|
-
return res;
|
|
3427
|
-
});
|
|
3428
|
-
const steward = createSteward({
|
|
3429
|
-
actorRef: "system:test",
|
|
3430
|
-
retries: { sleep: () => Promise.resolve() },
|
|
3431
|
-
...options.steward,
|
|
3432
|
-
env: {},
|
|
3433
|
-
baseUrl,
|
|
3434
|
-
apiKey,
|
|
3435
|
-
adminBaseUrl: baseUrl,
|
|
3436
|
-
adminApiKey,
|
|
3437
|
-
fetch: fakeFetch
|
|
3438
|
-
});
|
|
3439
|
-
let handler = webhook?.handler ?? null;
|
|
3440
|
-
function requireWebhook(name) {
|
|
3441
|
-
if (!webhook) throw new Error(`simulate.${name}: createFakeSteward({webhook}) verilmedi`);
|
|
3442
|
-
if (!handler) throw new Error(`simulate.${name}: webhook alıcısı bağlanmadı (webhook.handler ya da fake.connect(handler))`);
|
|
3443
|
-
return {
|
|
3444
|
-
options: webhook,
|
|
3445
|
-
handler
|
|
3446
|
-
};
|
|
3447
|
-
}
|
|
3448
|
-
async function send(hook, event) {
|
|
3449
|
-
const signed = signBody(event, secretsOf(hook.options.secret));
|
|
3450
|
-
const response = await hook.handler(signed.request(hook.options.url ?? "http://product.test/steward/events"));
|
|
3451
|
-
return {
|
|
3452
|
-
event,
|
|
3453
|
-
body: signed.body,
|
|
3454
|
-
headers: signed.headers,
|
|
3455
|
-
response
|
|
3456
|
-
};
|
|
3457
|
-
}
|
|
3458
|
-
async function flushOne(hook, row) {
|
|
3459
|
-
const d = row.delivery;
|
|
3460
|
-
d.attempts += 1;
|
|
3461
|
-
const retryAt = () => w.now() + (RETRY_SCHEDULE_SECONDS[d.attempts - 1] ?? 43200) * 1e3;
|
|
3462
|
-
let delivery;
|
|
3463
|
-
try {
|
|
3464
|
-
delivery = await send(hook, row.payload);
|
|
3465
|
-
} catch (error) {
|
|
3466
|
-
Object.assign(d, {
|
|
3467
|
-
lastHttpStatus: null,
|
|
3468
|
-
lastError: "network_error",
|
|
3469
|
-
nextAttemptAt: retryAt()
|
|
3470
|
-
});
|
|
3471
|
-
throw error;
|
|
3472
|
-
}
|
|
3473
|
-
const status = delivery.response.status;
|
|
3474
|
-
if (status >= 200 && status < 300) Object.assign(d, {
|
|
3475
|
-
status: "delivered",
|
|
3476
|
-
deliveredAt: w.now(),
|
|
3477
|
-
lastHttpStatus: status,
|
|
3478
|
-
lastError: null
|
|
3479
|
-
});
|
|
3480
|
-
else Object.assign(d, {
|
|
3481
|
-
lastHttpStatus: status,
|
|
3482
|
-
lastError: `http_${status}`,
|
|
3483
|
-
nextAttemptAt: retryAt()
|
|
3484
|
-
});
|
|
3485
|
-
return delivery;
|
|
3486
|
-
}
|
|
3487
|
-
function portalSession(name, id) {
|
|
3488
|
-
const session = livePortalSession(w, id);
|
|
3489
|
-
if (!session) throw new Error(`simulate.${name}: portal oturumu yok ya da süresi dolmuş ${id}`);
|
|
3490
|
-
return session;
|
|
3491
|
-
}
|
|
3492
|
-
/** Güncel durumla (fixture detayı hesaba uyarlanmış) sözleşmeye uyan event. */
|
|
3493
|
-
function eventFor(type, ref, state, cause, createdAt) {
|
|
3494
|
-
let detail;
|
|
3495
|
-
if (type === "account.state_changed") detail = {
|
|
3496
|
-
cause: cause ?? "account.refreshed",
|
|
3497
|
-
causeEventId: null
|
|
3498
|
-
};
|
|
3499
|
-
else {
|
|
3500
|
-
detail = { ...eventFixture(type).data.detail };
|
|
3501
|
-
const sub = state.subscription;
|
|
3502
|
-
const known = {
|
|
3503
|
-
subscriptionId: sub?.id,
|
|
3504
|
-
planCode: sub?.planCode ?? state.planCode,
|
|
3505
|
-
interval: sub?.interval,
|
|
3506
|
-
amountMinor: sub?.amountMinor,
|
|
3507
|
-
currency: sub?.currency,
|
|
3508
|
-
checkoutSessionId: state.openCheckout?.id
|
|
3509
|
-
};
|
|
3510
|
-
const shape = EVENT_DETAIL_SCHEMAS[type].shape;
|
|
3511
|
-
for (const [key, value] of Object.entries(known)) if (key in shape && value !== void 0) detail[key] = value;
|
|
3512
|
-
}
|
|
3513
|
-
return envelope(w, ref, state, {
|
|
3514
|
-
type,
|
|
3515
|
-
detail
|
|
3516
|
-
}, `evt_${w.id()}`, createdAt);
|
|
3517
|
-
}
|
|
3518
|
-
return {
|
|
3519
|
-
baseUrl,
|
|
3520
|
-
apiKey,
|
|
3521
|
-
adminApiKey,
|
|
3522
|
-
fetch: fakeFetch,
|
|
3523
|
-
steward,
|
|
3524
|
-
requests,
|
|
3525
|
-
simulate: {
|
|
3526
|
-
async checkoutCompleted(sessionId) {
|
|
3527
|
-
completeCheckout(w, sessionRow(w, sessionId));
|
|
3528
|
-
return checkoutSessionResponse(w, sessionId);
|
|
3529
|
-
},
|
|
3530
|
-
async checkoutFailed(sessionId, opts = {}) {
|
|
3531
|
-
failCheckout(w, sessionRow(w, sessionId), opts.reason ?? "fake:card_declined");
|
|
3532
|
-
return checkoutSessionResponse(w, sessionId);
|
|
3533
|
-
},
|
|
3534
|
-
async completeHostedCheckout(url, opts = {}) {
|
|
3535
|
-
const outcome = opts.outcome ?? "succeeded";
|
|
3536
|
-
const page = await fakeFetch(url);
|
|
3537
|
-
await page.text();
|
|
3538
|
-
if (page.status !== 200) throw new Error(`simulate.completeHostedCheckout: sayfa ${page.status} döndü (oturum açık değil ya da adres yanlış)`);
|
|
3539
|
-
const pageUrl = new URL(url);
|
|
3540
|
-
const id = decodeURIComponent(pageUrl.pathname.split("/").at(-1) ?? "");
|
|
3541
|
-
const res = await fakeFetch(new URL(`${encodeURIComponent(id)}/pay`, pageUrl), {
|
|
3542
|
-
method: "POST",
|
|
3543
|
-
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
3544
|
-
body: new URLSearchParams({
|
|
3545
|
-
k: pageUrl.searchParams.get("k") ?? "",
|
|
3546
|
-
outcome
|
|
3547
|
-
})
|
|
3548
|
-
});
|
|
3549
|
-
const location = res.headers.get("location");
|
|
3550
|
-
if (res.status !== 303 || location === null) throw new Error(`simulate.completeHostedCheckout: 303 bekleniyordu, ${res.status} geldi`);
|
|
3551
|
-
return {
|
|
3552
|
-
id,
|
|
3553
|
-
redirectUrl: location,
|
|
3554
|
-
session: checkoutSessionResponse(w, id)
|
|
3555
|
-
};
|
|
3556
|
-
},
|
|
3557
|
-
async checkoutCanceled(sessionId) {
|
|
3558
|
-
const row = sessionRow(w, sessionId);
|
|
3559
|
-
cancelCheckout(w, row);
|
|
3560
|
-
return checkoutSessionResponse(w, sessionId);
|
|
3561
|
-
},
|
|
3562
|
-
async renewal(ref, opts = {}) {
|
|
3563
|
-
const sub = liveSubscription(w, ref);
|
|
3564
|
-
if (!sub) throw new Error(`simulate.renewal: ${ref} için canlı abonelik yok`);
|
|
3565
|
-
if (sub.cancelAtPeriodEnd) throw new Error(`simulate.renewal: ${ref} aboneliği dönem sonunda bitecek, yenilenmez (simulate.advance)`);
|
|
3566
|
-
const now = w.now();
|
|
3567
|
-
advanceTo(w, opts.advanceDays !== void 0 ? now + opts.advanceDays * DAY_MS : Math.max(now, sub.currentPeriodEnd ?? now));
|
|
3568
|
-
const current = liveSubscription(w, ref);
|
|
3569
|
-
if (!current) throw new Error(`simulate.renewal: ${ref} aboneliği saat ilerlerken sona erdi`);
|
|
3570
|
-
applyRenewalPayment(w, current, opts.fail ? "failed" : "succeeded");
|
|
3571
|
-
return storedState(w, ref);
|
|
3572
|
-
},
|
|
3573
|
-
async portalCancel(portalSessionId) {
|
|
3574
|
-
const session = portalSession("portalCancel", portalSessionId);
|
|
3575
|
-
return {
|
|
3576
|
-
result: portalCancel(w, session),
|
|
3577
|
-
account: storedState(w, session.accountRef)
|
|
3578
|
-
};
|
|
3579
|
-
},
|
|
3580
|
-
async portalRetryPayment(portalSessionId) {
|
|
3581
|
-
const session = portalSession("portalRetryPayment", portalSessionId);
|
|
3582
|
-
return {
|
|
3583
|
-
result: portalRetryPayment(w, session),
|
|
3584
|
-
account: storedState(w, session.accountRef)
|
|
3585
|
-
};
|
|
3586
|
-
},
|
|
3587
|
-
async advance(duration = {}) {
|
|
3588
|
-
const ms = (duration.days ?? 0) * DAY_MS + (duration.hours ?? 0) * 36e5 + (duration.minutes ?? 0) * 6e4;
|
|
3589
|
-
if (!(ms >= 0)) throw new Error("simulate.advance: süre negatif olamaz");
|
|
3590
|
-
advanceTo(w, w.now() + ms);
|
|
3591
|
-
},
|
|
3592
|
-
outage(on, opts = {}) {
|
|
3593
|
-
outage = on ? opts.mode ?? "network" : null;
|
|
3594
|
-
},
|
|
3595
|
-
async deliver(type, opts) {
|
|
3596
|
-
const hook = requireWebhook("deliver");
|
|
3597
|
-
const row = w.accounts.get(opts.ref);
|
|
3598
|
-
if (!row?.state) throw new Error(`simulate.deliver: hesap yok ${opts.ref}`);
|
|
3599
|
-
const current = eventFor(type, opts.ref, row.state, opts.cause, w.now());
|
|
3600
|
-
const sequence = [current];
|
|
3601
|
-
if (opts.duplicate) sequence.push(current);
|
|
3602
|
-
if (opts.outOfOrder) {
|
|
3603
|
-
const previous = row.history.at(-2);
|
|
3604
|
-
if (!previous) throw new Error(`simulate.deliver: ${opts.ref} için önceki version yok (outOfOrder)`);
|
|
3605
|
-
sequence.push(eventFor(type, opts.ref, previous, opts.cause, Date.parse(previous.updatedAt)));
|
|
3606
|
-
}
|
|
3607
|
-
const out = [];
|
|
3608
|
-
for (const event of sequence) out.push(await send(hook, event));
|
|
3609
|
-
return out;
|
|
3610
|
-
},
|
|
3611
|
-
async flush() {
|
|
3612
|
-
const hook = requireWebhook("flush");
|
|
3613
|
-
const blocked = /* @__PURE__ */ new Set();
|
|
3614
|
-
const out = [];
|
|
3615
|
-
for (let i = 0; i < w.events.length; i++) {
|
|
3616
|
-
const row = w.events[i];
|
|
3617
|
-
if (row.delivery?.status !== "pending" || blocked.has(row.accountRef)) continue;
|
|
3618
|
-
const delivery = await flushOne(hook, row);
|
|
3619
|
-
if (row.delivery.status === "pending") blocked.add(row.accountRef);
|
|
3620
|
-
out.push(delivery);
|
|
3621
|
-
}
|
|
3622
|
-
return out;
|
|
3623
|
-
},
|
|
3624
|
-
redeliver(eventId) {
|
|
3625
|
-
if (!redeliverEvent(w, eventId)) throw new Error(`simulate.redeliver: unknown event ${eventId}`);
|
|
3626
|
-
}
|
|
3627
|
-
},
|
|
3628
|
-
connect(next) {
|
|
3629
|
-
if (!webhook) throw new Error("fake.connect: createFakeSteward({webhook: {secret}}) verilmedi (sır ve filtre oluşturmada)");
|
|
3630
|
-
if (typeof next !== "function") throw new Error("fake.connect: alıcı fonksiyon olmalı");
|
|
3631
|
-
handler = next;
|
|
3632
|
-
},
|
|
3633
|
-
events: (ref) => w.events.filter((e) => ref === void 0 || e.accountRef === ref).map((e) => e.payload),
|
|
3634
|
-
now: () => new Date(w.now())
|
|
3635
|
-
};
|
|
3636
|
-
}
|
|
3637
|
-
//#endregion
|
|
3638
|
-
export { DEFAULT_DUNNING_POLICY, createFakeSteward, signedEvent };
|