@btst/plugin-affiliate 0.0.0-stage → 1.0.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/LICENSE.md +17 -0
- package/dist/api/getters.cjs +182 -0
- package/dist/api/getters.mjs +166 -0
- package/dist/api/index.cjs +49 -0
- package/dist/api/index.d.cts +975 -0
- package/dist/api/index.d.mts +975 -0
- package/dist/api/index.d.ts +975 -0
- package/dist/api/index.mjs +7 -0
- package/dist/api/mutations.cjs +362 -0
- package/dist/api/mutations.mjs +352 -0
- package/dist/api/operations.cjs +328 -0
- package/dist/api/operations.mjs +326 -0
- package/dist/api/plugin.cjs +146 -0
- package/dist/api/plugin.mjs +144 -0
- package/dist/attribution.cjs +92 -0
- package/dist/attribution.d.cts +20 -0
- package/dist/attribution.d.mts +20 -0
- package/dist/attribution.d.ts +20 -0
- package/dist/attribution.mjs +82 -0
- package/dist/client/components/index.cjs +12 -0
- package/dist/client/components/index.d.cts +13 -0
- package/dist/client/components/index.d.mts +13 -0
- package/dist/client/components/index.d.ts +13 -0
- package/dist/client/components/index.mjs +1 -0
- package/dist/client/components/pages/affiliate-apply-page.internal.cjs +59 -0
- package/dist/client/components/pages/affiliate-apply-page.internal.mjs +57 -0
- package/dist/client/components/pages/affiliate-detail-page.internal.cjs +112 -0
- package/dist/client/components/pages/affiliate-detail-page.internal.mjs +110 -0
- package/dist/client/components/pages/affiliate-portal-page.internal.cjs +252 -0
- package/dist/client/components/pages/affiliate-portal-page.internal.mjs +250 -0
- package/dist/client/components/pages/affiliates-page.internal.cjs +160 -0
- package/dist/client/components/pages/affiliates-page.internal.mjs +158 -0
- package/dist/client/components/pages/commissions-page.internal.cjs +151 -0
- package/dist/client/components/pages/commissions-page.internal.mjs +149 -0
- package/dist/client/components/pages/payouts-page.internal.cjs +230 -0
- package/dist/client/components/pages/payouts-page.internal.mjs +228 -0
- package/dist/client/components/pages.cjs +145 -0
- package/dist/client/components/pages.mjs +138 -0
- package/dist/client/components/shared.cjs +331 -0
- package/dist/client/components/shared.mjs +316 -0
- package/dist/client/constants.cjs +5 -0
- package/dist/client/constants.mjs +3 -0
- package/dist/client/hooks/index.cjs +77 -0
- package/dist/client/hooks/index.d.cts +39 -0
- package/dist/client/hooks/index.d.mts +39 -0
- package/dist/client/hooks/index.d.ts +39 -0
- package/dist/client/hooks/index.mjs +61 -0
- package/dist/client/index.cjs +7 -0
- package/dist/client/index.d.cts +110 -0
- package/dist/client/index.d.mts +110 -0
- package/dist/client/index.d.ts +110 -0
- package/dist/client/index.mjs +1 -0
- package/dist/client/plugin.cjs +77 -0
- package/dist/client/plugin.mjs +75 -0
- package/dist/client.css +311 -0
- package/dist/commission.cjs +24 -0
- package/dist/commission.mjs +22 -0
- package/dist/db.cjs +74 -0
- package/dist/db.mjs +72 -0
- package/dist/permissions.cjs +53 -0
- package/dist/permissions.d.cts +78 -0
- package/dist/permissions.d.mts +78 -0
- package/dist/permissions.d.ts +78 -0
- package/dist/permissions.mjs +51 -0
- package/dist/query-keys.cjs +152 -0
- package/dist/query-keys.d.cts +394 -0
- package/dist/query-keys.d.mts +394 -0
- package/dist/query-keys.d.ts +394 -0
- package/dist/query-keys.mjs +149 -0
- package/dist/schemas.cjs +95 -0
- package/dist/schemas.mjs +77 -0
- package/dist/style.css +313 -0
- package/dist/types.cjs +2 -0
- package/dist/types.d.cts +176 -0
- package/dist/types.d.mts +176 -0
- package/dist/types.d.ts +176 -0
- package/dist/types.mjs +1 -0
- package/package.json +159 -3
- package/README.md +0 -3
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { affiliateBackendPlugin } from './plugin.mjs';
|
|
2
|
+
export { affiliateSchema } from '../db.mjs';
|
|
3
|
+
export { affiliatePermissions } from '../permissions.mjs';
|
|
4
|
+
export { allRows, exportPayouts, getAffiliate, getAffiliateByCode, getAffiliateByUserId, getAffiliateStats, getCommission, getConversionByExternalRef, getPayout, getPortal, getReferralClick, listAffiliates, listCommissions, listPayouts, serializeAffiliateRecord } from './getters.mjs';
|
|
5
|
+
export { createAffiliate, createConversion, createPayout, deleteAffiliate, trackClick, updateAffiliate, updateCommission, updatePayout, withAffiliateTransaction } from './mutations.mjs';
|
|
6
|
+
export { AFFILIATE_COOKIE_NAME, attributionFromRequest, createReferralLink, createTrackingLink, getReferralRedirect, isClickWithinWindow, referralCodeFromURL, safeLandingPath, selectAttributionClick } from '../attribution.mjs';
|
|
7
|
+
export { calculateCommission } from '../commission.mjs';
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const api = require('@btst/stack/plugins/api');
|
|
4
|
+
const attribution = require('../attribution.cjs');
|
|
5
|
+
const commission = require('../commission.cjs');
|
|
6
|
+
const schemas = require('../schemas.cjs');
|
|
7
|
+
const getters = require('./getters.cjs');
|
|
8
|
+
|
|
9
|
+
function assertAffiliateSnapshot(current, expected) {
|
|
10
|
+
if (expected && (current.id !== expected.id || current.userId !== expected.userId || current.status !== expected.status)) {
|
|
11
|
+
fail("Affiliate changed after authorization");
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
const queues = /* @__PURE__ */ new WeakMap();
|
|
15
|
+
function fail(message, status = 409) {
|
|
16
|
+
throw new api.OperationHttpError(status, message, "AFFILIATE_CONFLICT");
|
|
17
|
+
}
|
|
18
|
+
async function withAffiliateTransaction(adapter, affiliateId, run) {
|
|
19
|
+
const config = adapter.options?.adapterConfig;
|
|
20
|
+
if (!config?.transaction)
|
|
21
|
+
fail("Affiliate writes require an adapter with transactions enabled", 503);
|
|
22
|
+
let pending = queues.get(adapter);
|
|
23
|
+
if (!pending) {
|
|
24
|
+
pending = /* @__PURE__ */ new Map();
|
|
25
|
+
queues.set(adapter, pending);
|
|
26
|
+
}
|
|
27
|
+
const key = config.adapterId === "memory" ? "*" : affiliateId ?? "create";
|
|
28
|
+
const previous = pending.get(key) ?? Promise.resolve();
|
|
29
|
+
const operation = previous.catch(() => {
|
|
30
|
+
}).then(
|
|
31
|
+
() => adapter.transaction(async (tx) => {
|
|
32
|
+
if (affiliateId) {
|
|
33
|
+
const locked = await tx.update({
|
|
34
|
+
model: "affiliate",
|
|
35
|
+
where: [{ field: "id", value: affiliateId, operator: "eq" }],
|
|
36
|
+
update: { lockVersion: crypto.randomUUID() }
|
|
37
|
+
});
|
|
38
|
+
if (!locked) fail("Affiliate not found", 404);
|
|
39
|
+
}
|
|
40
|
+
return run(tx);
|
|
41
|
+
})
|
|
42
|
+
);
|
|
43
|
+
pending.set(key, operation);
|
|
44
|
+
try {
|
|
45
|
+
return await operation;
|
|
46
|
+
} finally {
|
|
47
|
+
if (pending.get(key) === operation) pending.delete(key);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async function ensureUniqueAffiliate(adapter, input, excludeId) {
|
|
51
|
+
for (const field of ["code", "email", "userId"]) {
|
|
52
|
+
const value = input[field];
|
|
53
|
+
if (!value) continue;
|
|
54
|
+
const existing = await adapter.findOne({
|
|
55
|
+
model: "affiliate",
|
|
56
|
+
where: [{ field, value, operator: "eq" }]
|
|
57
|
+
});
|
|
58
|
+
if (existing && existing.id !== excludeId)
|
|
59
|
+
fail(`Affiliate ${field} is already in use`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async function createAffiliate(adapter, input) {
|
|
63
|
+
const data = schemas.createAffiliateSchema.parse(input);
|
|
64
|
+
return withAffiliateTransaction(adapter, null, async (tx) => {
|
|
65
|
+
const values = {
|
|
66
|
+
...data,
|
|
67
|
+
email: data.email.toLowerCase(),
|
|
68
|
+
code: data.code ?? `partner-${crypto.randomUUID().replaceAll("-", "").slice(0, 16)}`
|
|
69
|
+
};
|
|
70
|
+
await ensureUniqueAffiliate(tx, values);
|
|
71
|
+
return getters.serializeAffiliateRecord(
|
|
72
|
+
await tx.create({
|
|
73
|
+
model: "affiliate",
|
|
74
|
+
data: {
|
|
75
|
+
...values,
|
|
76
|
+
status: "pending",
|
|
77
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
78
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
async function updateAffiliate(adapter, id, input, expected) {
|
|
85
|
+
const data = schemas.updateAffiliateSchema.parse(input);
|
|
86
|
+
return withAffiliateTransaction(adapter, id, async (tx) => {
|
|
87
|
+
const current = await getters.getAffiliate(tx, id);
|
|
88
|
+
if (!current) fail("Affiliate not found", 404);
|
|
89
|
+
assertAffiliateSnapshot(current, expected);
|
|
90
|
+
const values = {
|
|
91
|
+
...data,
|
|
92
|
+
...data.email ? { email: data.email.toLowerCase() } : {}
|
|
93
|
+
};
|
|
94
|
+
await ensureUniqueAffiliate(tx, values, id);
|
|
95
|
+
if (data.status && data.status !== current.status) {
|
|
96
|
+
const allowed = current.status === "pending" ? ["approved", "suspended"] : current.status === "approved" ? ["suspended"] : ["approved"];
|
|
97
|
+
if (!allowed.includes(data.status))
|
|
98
|
+
fail("Invalid affiliate status transition");
|
|
99
|
+
}
|
|
100
|
+
const ruleType = data.commissionType === void 0 ? current.commissionType : data.commissionType;
|
|
101
|
+
const ruleValue = data.commissionValue === void 0 ? current.commissionValue : data.commissionValue;
|
|
102
|
+
if (ruleType == null !== (ruleValue == null))
|
|
103
|
+
fail("Commission override requires both type and value", 400);
|
|
104
|
+
if (ruleType != null && ruleValue != null) {
|
|
105
|
+
try {
|
|
106
|
+
commission.calculateCommission(0, ruleType, ruleValue);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
fail(
|
|
109
|
+
error instanceof Error ? error.message : "Invalid commission rule",
|
|
110
|
+
400
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return getters.serializeAffiliateRecord(
|
|
115
|
+
await tx.update({
|
|
116
|
+
model: "affiliate",
|
|
117
|
+
where: [{ field: "id", value: id, operator: "eq" }],
|
|
118
|
+
update: { ...values, updatedAt: /* @__PURE__ */ new Date() }
|
|
119
|
+
})
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
async function deleteAffiliate(adapter, id, expected) {
|
|
124
|
+
return withAffiliateTransaction(adapter, id, async (tx) => {
|
|
125
|
+
const current = await getters.getAffiliate(tx, id);
|
|
126
|
+
if (!current) fail("Affiliate not found", 404);
|
|
127
|
+
assertAffiliateSnapshot(current, expected);
|
|
128
|
+
for (const model of [
|
|
129
|
+
"affiliateConversion",
|
|
130
|
+
"affiliateCommission",
|
|
131
|
+
"affiliatePayout"
|
|
132
|
+
]) {
|
|
133
|
+
if (await tx.count({
|
|
134
|
+
model,
|
|
135
|
+
where: [{ field: "affiliateId", value: id, operator: "eq" }]
|
|
136
|
+
}))
|
|
137
|
+
fail(
|
|
138
|
+
"Affiliates with financial history must be suspended instead of deleted"
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
await tx.deleteMany({
|
|
142
|
+
model: "referralClick",
|
|
143
|
+
where: [{ field: "affiliateId", value: id, operator: "eq" }]
|
|
144
|
+
});
|
|
145
|
+
await tx.delete({
|
|
146
|
+
model: "affiliate",
|
|
147
|
+
where: [{ field: "id", value: id, operator: "eq" }]
|
|
148
|
+
});
|
|
149
|
+
return { success: true };
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
async function trackClick(adapter, input) {
|
|
153
|
+
const data = schemas.trackClickSchema.parse(input);
|
|
154
|
+
const landingPath = attribution.safeLandingPath(data.landingPath);
|
|
155
|
+
const affiliate = await getters.getAffiliateByCode(adapter, data.code);
|
|
156
|
+
if (!affiliate || affiliate.status !== "approved")
|
|
157
|
+
fail("Approved affiliate not found", 404);
|
|
158
|
+
return withAffiliateTransaction(adapter, affiliate.id, async (tx) => {
|
|
159
|
+
if ((await getters.getAffiliate(tx, affiliate.id))?.status !== "approved")
|
|
160
|
+
fail("Approved affiliate not found", 404);
|
|
161
|
+
return getters.serializeAffiliateRecord(
|
|
162
|
+
await tx.create({
|
|
163
|
+
model: "referralClick",
|
|
164
|
+
data: {
|
|
165
|
+
...data,
|
|
166
|
+
landingPath,
|
|
167
|
+
affiliateId: affiliate.id,
|
|
168
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
169
|
+
}
|
|
170
|
+
})
|
|
171
|
+
);
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
async function previousConversion(adapter, input) {
|
|
175
|
+
if (!input.externalRef) return null;
|
|
176
|
+
const conversion = await getters.getConversionByExternalRef(
|
|
177
|
+
adapter,
|
|
178
|
+
input.externalRef
|
|
179
|
+
);
|
|
180
|
+
if (!conversion) return null;
|
|
181
|
+
if (conversion.amount !== input.amount || conversion.currency !== (input.currency ?? "USD"))
|
|
182
|
+
fail("Conversion external reference already has different values");
|
|
183
|
+
const commission = await adapter.findOne({
|
|
184
|
+
model: "affiliateCommission",
|
|
185
|
+
where: [{ field: "conversionId", value: conversion.id, operator: "eq" }]
|
|
186
|
+
});
|
|
187
|
+
if (!commission) fail("Conversion commission is missing");
|
|
188
|
+
return {
|
|
189
|
+
conversion,
|
|
190
|
+
commission: getters.serializeAffiliateRecord(commission)
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
async function createConversion(adapter, input, options = {}) {
|
|
194
|
+
const data = schemas.createConversionSchema.parse(input);
|
|
195
|
+
const existing = await previousConversion(adapter, data);
|
|
196
|
+
if (existing) return existing;
|
|
197
|
+
if (!data.attribution) return null;
|
|
198
|
+
const click = data.attribution.clickId ? await getters.getReferralClick(adapter, data.attribution.clickId) : null;
|
|
199
|
+
if (data.attribution.clickId && (!click || !attribution.isClickWithinWindow(click, options.attributionWindowDays)))
|
|
200
|
+
return null;
|
|
201
|
+
const affiliate = click ? await getters.getAffiliate(adapter, click.affiliateId) : data.attribution.code ? await getters.getAffiliateByCode(adapter, data.attribution.code) : null;
|
|
202
|
+
if (!affiliate || affiliate.status !== "approved") return null;
|
|
203
|
+
if (click && data.attribution.code && click.code !== data.attribution.code)
|
|
204
|
+
fail("Attribution code does not match referral click", 400);
|
|
205
|
+
try {
|
|
206
|
+
return await withAffiliateTransaction(adapter, affiliate.id, async (tx) => {
|
|
207
|
+
const duplicate = await previousConversion(tx, data);
|
|
208
|
+
if (duplicate) return duplicate;
|
|
209
|
+
const current = await getters.getAffiliate(tx, affiliate.id);
|
|
210
|
+
if (!current || current.status !== "approved") return null;
|
|
211
|
+
const type = current.commissionType ?? options.defaultCommissionType ?? "percent";
|
|
212
|
+
const value = current.commissionValue ?? options.defaultCommissionValue ?? 20;
|
|
213
|
+
const amount = commission.calculateCommission(data.amount, type, value);
|
|
214
|
+
const conversion = getters.serializeAffiliateRecord(
|
|
215
|
+
await tx.create({
|
|
216
|
+
model: "affiliateConversion",
|
|
217
|
+
data: {
|
|
218
|
+
affiliateId: affiliate.id,
|
|
219
|
+
...click ? { clickId: click.id } : {},
|
|
220
|
+
...data.externalRef ? { externalRef: data.externalRef } : {},
|
|
221
|
+
amount: data.amount,
|
|
222
|
+
currency: data.currency,
|
|
223
|
+
status: "pending",
|
|
224
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
225
|
+
}
|
|
226
|
+
})
|
|
227
|
+
);
|
|
228
|
+
const commission$1 = getters.serializeAffiliateRecord(
|
|
229
|
+
await tx.create({
|
|
230
|
+
model: "affiliateCommission",
|
|
231
|
+
data: {
|
|
232
|
+
affiliateId: affiliate.id,
|
|
233
|
+
conversionId: conversion.id,
|
|
234
|
+
amount,
|
|
235
|
+
currency: data.currency,
|
|
236
|
+
status: "pending",
|
|
237
|
+
payoutId: "",
|
|
238
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
239
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
240
|
+
}
|
|
241
|
+
})
|
|
242
|
+
);
|
|
243
|
+
return { conversion, commission: commission$1 };
|
|
244
|
+
});
|
|
245
|
+
} catch (error) {
|
|
246
|
+
const duplicate = await previousConversion(adapter, data);
|
|
247
|
+
if (duplicate) return duplicate;
|
|
248
|
+
throw error;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
async function updateCommission(adapter, id, input, expected) {
|
|
252
|
+
const data = schemas.updateCommissionSchema.parse(input);
|
|
253
|
+
const found = await getters.getCommission(adapter, id);
|
|
254
|
+
if (!found) fail("Commission not found", 404);
|
|
255
|
+
return withAffiliateTransaction(adapter, found.affiliateId, async (tx) => {
|
|
256
|
+
const current = await getters.getCommission(tx, id);
|
|
257
|
+
if (!current) fail("Commission not found", 404);
|
|
258
|
+
if (expected && (current.id !== expected.id || current.affiliateId !== expected.affiliateId || current.status !== expected.status))
|
|
259
|
+
fail("Commission changed after authorization");
|
|
260
|
+
if (current.status === data.status) return current;
|
|
261
|
+
if (current.payoutId || !["pending", "approved"].includes(current.status) || current.status === "approved" && data.status !== "voided")
|
|
262
|
+
fail("Commission is paid, reserved, or cannot transition to that status");
|
|
263
|
+
const result = await tx.update({
|
|
264
|
+
model: "affiliateCommission",
|
|
265
|
+
where: [{ field: "id", value: id, operator: "eq" }],
|
|
266
|
+
update: { status: data.status, updatedAt: /* @__PURE__ */ new Date() }
|
|
267
|
+
});
|
|
268
|
+
await tx.update({
|
|
269
|
+
model: "affiliateConversion",
|
|
270
|
+
where: [{ field: "id", value: current.conversionId, operator: "eq" }],
|
|
271
|
+
update: { status: data.status === "approved" ? "approved" : "rejected" }
|
|
272
|
+
});
|
|
273
|
+
return getters.serializeAffiliateRecord(result);
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
async function createPayout(adapter, input) {
|
|
277
|
+
const data = schemas.createPayoutSchema.parse(input);
|
|
278
|
+
return withAffiliateTransaction(adapter, data.affiliateId, async (tx) => {
|
|
279
|
+
const affiliate = await getters.getAffiliate(tx, data.affiliateId);
|
|
280
|
+
if (!affiliate || affiliate.status !== "approved")
|
|
281
|
+
fail("Payouts require an approved affiliate");
|
|
282
|
+
const all = await getters.allRows(tx, "affiliateCommission", [
|
|
283
|
+
{ field: "affiliateId", value: data.affiliateId, operator: "eq" },
|
|
284
|
+
{ field: "currency", value: data.currency, operator: "eq" },
|
|
285
|
+
{ field: "status", value: "approved", operator: "eq" },
|
|
286
|
+
{ field: "payoutId", value: "", operator: "eq" }
|
|
287
|
+
]);
|
|
288
|
+
const selected = data.commissionIds ? all.filter((item) => data.commissionIds?.includes(item.id)) : all;
|
|
289
|
+
if (!selected.length || data.commissionIds && selected.length !== new Set(data.commissionIds).size)
|
|
290
|
+
fail("Select available approved commissions in one currency");
|
|
291
|
+
const amount = selected.reduce(
|
|
292
|
+
(total, item) => total + BigInt(item.amount),
|
|
293
|
+
0n
|
|
294
|
+
);
|
|
295
|
+
if (amount > BigInt(Number.MAX_SAFE_INTEGER))
|
|
296
|
+
fail("Payout exceeds safe integer range", 400);
|
|
297
|
+
const payout = getters.serializeAffiliateRecord(
|
|
298
|
+
await tx.create({
|
|
299
|
+
model: "affiliatePayout",
|
|
300
|
+
data: {
|
|
301
|
+
affiliateId: data.affiliateId,
|
|
302
|
+
amount: Number(amount),
|
|
303
|
+
currency: data.currency,
|
|
304
|
+
status: "pending",
|
|
305
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
306
|
+
}
|
|
307
|
+
})
|
|
308
|
+
);
|
|
309
|
+
for (const item of selected) {
|
|
310
|
+
await tx.update({
|
|
311
|
+
model: "affiliateCommission",
|
|
312
|
+
where: [{ field: "id", value: item.id, operator: "eq" }],
|
|
313
|
+
update: { payoutId: payout.id, updatedAt: /* @__PURE__ */ new Date() }
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
return payout;
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
async function updatePayout(adapter, id, input, expected) {
|
|
320
|
+
const data = schemas.updatePayoutSchema.parse(input);
|
|
321
|
+
const found = await getters.getPayout(adapter, id);
|
|
322
|
+
if (!found) fail("Payout not found", 404);
|
|
323
|
+
return withAffiliateTransaction(adapter, found.affiliateId, async (tx) => {
|
|
324
|
+
const current = await getters.getPayout(tx, id);
|
|
325
|
+
if (!current) fail("Payout not found", 404);
|
|
326
|
+
if (expected && (current.id !== expected.id || current.affiliateId !== expected.affiliateId || current.status !== expected.status))
|
|
327
|
+
fail("Payout changed after authorization");
|
|
328
|
+
if (current.status === data.status) return current;
|
|
329
|
+
if (current.status !== "pending")
|
|
330
|
+
fail("Only pending payouts can be marked sent or failed");
|
|
331
|
+
const commissions = await getters.allRows(tx, "affiliateCommission", [
|
|
332
|
+
{ field: "payoutId", value: id, operator: "eq" }
|
|
333
|
+
]);
|
|
334
|
+
for (const commission of commissions) {
|
|
335
|
+
await tx.update({
|
|
336
|
+
model: "affiliateCommission",
|
|
337
|
+
where: [{ field: "id", value: commission.id, operator: "eq" }],
|
|
338
|
+
update: data.status === "sent" ? { status: "paid", updatedAt: /* @__PURE__ */ new Date() } : { payoutId: "", updatedAt: /* @__PURE__ */ new Date() }
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
return getters.serializeAffiliateRecord(
|
|
342
|
+
await tx.update({
|
|
343
|
+
model: "affiliatePayout",
|
|
344
|
+
where: [{ field: "id", value: id, operator: "eq" }],
|
|
345
|
+
update: {
|
|
346
|
+
...data,
|
|
347
|
+
...data.status === "sent" ? { paidAt: /* @__PURE__ */ new Date() } : {}
|
|
348
|
+
}
|
|
349
|
+
})
|
|
350
|
+
);
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
exports.createAffiliate = createAffiliate;
|
|
355
|
+
exports.createConversion = createConversion;
|
|
356
|
+
exports.createPayout = createPayout;
|
|
357
|
+
exports.deleteAffiliate = deleteAffiliate;
|
|
358
|
+
exports.trackClick = trackClick;
|
|
359
|
+
exports.updateAffiliate = updateAffiliate;
|
|
360
|
+
exports.updateCommission = updateCommission;
|
|
361
|
+
exports.updatePayout = updatePayout;
|
|
362
|
+
exports.withAffiliateTransaction = withAffiliateTransaction;
|