@homie-rocks/studio 0.50.4 → 0.50.6
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/CHANGELOG.md +36 -0
- package/README.md +1 -2
- package/bin/homie-studio.mjs +7 -7
- package/functions/FUNCTIONS.md +69 -13
- package/guides/office/SKILL.md +17 -0
- package/guides/servers/SKILL.md +17 -0
- package/guides/shop/SKILL.md +4 -3
- package/lib/build.mjs +3 -4
- package/lib/cloudflare.mjs +8 -0
- package/lib/functions-build.mjs +10 -3
- package/lib/functions-cli.mjs +9 -3
- package/lib/parts-build.mjs +1 -1
- package/lib/parts.mjs +1 -1
- package/lib/purchase-links.mjs +1 -1
- package/lib/scaffold.mjs +18 -12
- package/lib/shop.mjs +2 -30
- package/lib/template-history.json +88 -1
- package/lib/worker-config.mjs +20 -3
- package/package.json +1 -1
- package/shop/SELLING.md +1 -1
- package/shop/SHOP.md +11 -12
- package/worker/build-id.mjs +1 -1
- package/worker/canonical-json.mjs +6 -0
- package/worker/customer-mcp.mjs +1 -1
- package/worker/customer-resources.mjs +1 -1
- package/worker/function-schedule.mjs +68 -0
- package/worker/functions-schema.mjs +13 -0
- package/worker/functions.mjs +45 -9
- package/worker/index.mjs +1 -14
- package/worker/mcp-tools.mjs +7 -3
- package/worker/office-page.mjs +1 -1
- package/worker/office.mjs +7 -14
- package/worker/parts-sale.mjs +1 -1
- package/worker/purchase-core.mjs +1 -1
- package/worker/purchase-crypto.mjs +1 -1
- package/worker/purchase-keys.mjs +1 -1
- package/worker/purchase-links.mjs +1 -1
- package/worker/purchase-owner.mjs +1 -1
- package/worker/purchase-routes.mjs +1 -1
- package/worker/purchase-store.mjs +1 -1
- package/worker/shop-page.mjs +5 -69
- package/worker/shop-rules.mjs +5 -33
- package/worker/shop-store.mjs +76 -56
- package/worker/shop.mjs +22 -78
- package/worker/version.mjs +1 -1
- package/worker/referrals.mjs +0 -460
package/worker/office.mjs
CHANGED
|
@@ -58,8 +58,8 @@ import { toolIdentity } from './tool-identity.mjs';
|
|
|
58
58
|
* taking a message down, happens at once.
|
|
59
59
|
*
|
|
60
60
|
* THE SHOP (0.24.0, worker/shop.mjs): /_studio/office/shop and /_studio/api/shop (sales, refunds, disputes, payouts as
|
|
61
|
-
* links to the studio's own Stripe, a CSV,
|
|
62
|
-
* from an office key it is ALWAYS an ask (the AI may only propose one)
|
|
61
|
+
* links to the studio's own Stripe, a CSV, order records). A refund is the owner's one tap;
|
|
62
|
+
* from an office key it is ALWAYS an ask (the AI may only propose one).
|
|
63
63
|
*/
|
|
64
64
|
import { seatsOf } from './seats.mjs';
|
|
65
65
|
import { diagnosticsOf, gatePlayers, roomLayout } from './gate.mjs';
|
|
@@ -71,7 +71,7 @@ import { esc, layout, notFoundPage } from './site.mjs';
|
|
|
71
71
|
import { confirmPage, lockedPage, officePage } from './office-page.mjs';
|
|
72
72
|
import { CHAT_MODES, CHAT_WHO, checkChatRules, publicChat } from './chat.mjs';
|
|
73
73
|
import { chatDay, chatOf, chatRowsOf, clearChatRules, dismissReport, reportsOf, writeChatRules } from './chat-store.mjs';
|
|
74
|
-
import {
|
|
74
|
+
import { checkRelease, performRelease, checkRefund, describeRefund, officeShopPage, ordersCsv, performRefund, shopOffice } from './shop.mjs';
|
|
75
75
|
import { orderById } from './shop-store.mjs';
|
|
76
76
|
import { checkLoungeAction, describeLounge, loungeNeedsAsk, loungeOffice, performLounge } from './lounge.mjs';
|
|
77
77
|
|
|
@@ -766,7 +766,6 @@ function checkAction(cat, op, body) {
|
|
|
766
766
|
}
|
|
767
767
|
case 'shop-release': return checkRelease(body);
|
|
768
768
|
case 'refund': return checkRefund(body);
|
|
769
|
-
case 'shop-settle': return checkSettle(body);
|
|
770
769
|
case 'agents-brain': {
|
|
771
770
|
if (!meta) return bad('game is one of this studio\'s game ids');
|
|
772
771
|
if (typeof body.server !== 'string' || !(body.server === 'public' || /^[a-z0-9][a-z0-9-]{1,19}$/.test(body.server))) return bad('server is the server\'s id');
|
|
@@ -868,7 +867,6 @@ export function describe(cat, a) {
|
|
|
868
867
|
case 'room-level': return `Set the AI in ${where} to ${levelName(a.level)} (level ${a.level}).`;
|
|
869
868
|
case 'shop-release': return `Release reservation ${a.order}: it stops counting toward the cap. We ask Stripe to expire an open Checkout Session. This does not refund a payment. A payment that still arrives grants the item, counts toward spending and can be refunded from the office.`;
|
|
870
869
|
case 'refund': return describeRefund(cat, a, a.item ? { item: a.item, amount: a.amount, currency: a.currency } : null);
|
|
871
|
-
case 'shop-settle': return `Mark what this studio owes ${a.via} for referrals in ${a.currency.toUpperCase()} as paid${a.ref ? ` (${a.ref})` : ''}: its owed lines are settled in the books.`;
|
|
872
870
|
case 'agents-brain': return ['workers-ai', 'owner-key'].includes(a.mode)
|
|
873
871
|
? `Let the AI guides on ${gname}'s server ${a.server} talk: they speak only the lines the game's own agents.json gives them (never free text), at most one line every 8 seconds, never about a person, and a player can quiet them. Their brain runs on ${a.mode === 'workers-ai' ? `this studio's own Workers AI (free allowance${a.budget !== undefined ? `; at most ${Math.round(a.budget).toLocaleString('en-US')} neurons a day` : ''})` : `your own AI provider key (claude-haiku-4-5, your money${a.budget !== undefined ? `, at most $${Number(a.budget).toFixed(2)} a day` : ', capped daily'})`}.`
|
|
874
872
|
: `Set the AI guides' brain on ${gname}'s server ${a.server} to ${a.mode}.`;
|
|
@@ -901,7 +899,7 @@ export async function needsAsk(env, cat, a) {
|
|
|
901
899
|
if (String(a.op).startsWith('lounge-')) return loungeNeedsAsk(env, cat, a, chatOpensUp);
|
|
902
900
|
if (DESTRUCTIVE.has(a.op)) return true;
|
|
903
901
|
// Money: the owner's AI may only propose a refund or a settlement; the owner says yes.
|
|
904
|
-
if (a.op === 'refund' || a.op === 'shop-
|
|
902
|
+
if (a.op === 'refund' || a.op === 'shop-release') return true;
|
|
905
903
|
if (a.op === 'server-close') return !a.reopen;
|
|
906
904
|
if (a.op === 'member') return a.remove === true;
|
|
907
905
|
if (a.op === 'server-set') {
|
|
@@ -1134,7 +1132,6 @@ export async function perform(env, cat, a) {
|
|
|
1134
1132
|
}
|
|
1135
1133
|
case 'shop-release': return performRelease(env, a, cat);
|
|
1136
1134
|
case 'refund': return performRefund(env, cat, a);
|
|
1137
|
-
case 'shop-settle': return settleReferrer(env, a);
|
|
1138
1135
|
default: return { ok: false, error: 'op' };
|
|
1139
1136
|
}
|
|
1140
1137
|
}
|
|
@@ -1284,24 +1281,20 @@ async function api(request, env, url, cat) {
|
|
|
1284
1281
|
const lounge = await loungeOffice(env, cat, url.origin);
|
|
1285
1282
|
return lounge ? json({ ok: true, lounge }) : json({ ok: false, error: 'no-lounge', message: 'This studio has no Lounge: add "lounge": true to studio.json and deploy.' }, 404);
|
|
1286
1283
|
}
|
|
1287
|
-
// The shop (0.24.0): the owner's view, the accountant's CSV,
|
|
1284
|
+
// The shop (0.24.0): the owner's view, the accountant's CSV, order records.
|
|
1288
1285
|
if (selling.enabled) { const result = await selling.officeRoutes(request, env, path, body); if (result) return result; }
|
|
1289
|
-
if (path === '/_studio/api/shop' && request.method === 'GET') return json(await shopOffice(env, cat, url.origin,
|
|
1286
|
+
if (path === '/_studio/api/shop' && request.method === 'GET') return json(await shopOffice(env, cat, url.origin, /^\d+$/.test(url.searchParams.get('itemCursor') ?? '') ? Number(url.searchParams.get('itemCursor')) : 0, /^\d+$/.test(url.searchParams.get('orderCursor') ?? '') ? Math.min(Number(url.searchParams.get('orderCursor')), Number.MAX_SAFE_INTEGER) : 0));
|
|
1290
1287
|
if (path === '/_studio/api/shop/orders.csv' && request.method === 'GET') {
|
|
1291
1288
|
try {
|
|
1292
1289
|
return new Response(await ordersCsv(env), { headers: { 'content-type': 'text/csv; charset=utf-8', 'cache-control': 'no-store, private', 'content-disposition': `attachment; filename="orders-${today()}.csv"` } });
|
|
1293
1290
|
} catch { return json({ ok: false, error: 'not-migrated', message: 'The shop needs migration 0008_studio_shop.sql (npm run deploy).' }, 503); }
|
|
1294
1291
|
}
|
|
1295
|
-
if (path === '/_studio/api/shop/lines' && request.method === 'GET') return json({ ok: true, ...(await linesOf(env, { via: url.searchParams.get('via'), currency: url.searchParams.get('currency'), cursor: url.searchParams.get('cursor') ?? '' })) });
|
|
1296
|
-
if (path === '/_studio/api/shop/received' && request.method === 'GET') return json(await receivedPages(env, url.searchParams.get('seller') ?? '', url.searchParams.get('period') ?? '', url.searchParams.get('cursor') ?? ''));
|
|
1297
|
-
if (path === '/_studio/api/shop/statements' && request.method === 'GET') return json(await statementsOut(env, cat, url.origin, url.searchParams.get('period'), url.searchParams.get('cursor') ?? ''));
|
|
1298
|
-
if (path === '/_studio/api/shop/statements/send' && request.method === 'POST') return json(await statementsSend(env, cat, url.origin, body.period, body.cursor ?? ''));
|
|
1299
1292
|
const OPS = {
|
|
1300
1293
|
'/_studio/api/chat/rules': 'chat-rules', '/_studio/api/chat/remove': 'chat-remove', '/_studio/api/chat/report': 'chat-report', '/_studio/api/chat/budget': 'chat-budget',
|
|
1301
1294
|
'/_studio/api/kick': 'kick', '/_studio/api/mute': 'mute', '/_studio/api/close': 'close', '/_studio/api/announce': 'announce', '/_studio/api/game': 'game',
|
|
1302
1295
|
'/_studio/api/servers': 'server-create', '/_studio/api/servers/set': 'server-set', '/_studio/api/servers/close': 'server-close', '/_studio/api/servers/member': 'member',
|
|
1303
1296
|
'/_studio/api/agents/pass': 'pass', '/_studio/api/room-level': 'room-level', '/_studio/api/agents/brain': 'agents-brain',
|
|
1304
|
-
'/_studio/api/shop/release': 'shop-release', '/_studio/api/shop/refund': 'refund',
|
|
1297
|
+
'/_studio/api/shop/release': 'shop-release', '/_studio/api/shop/refund': 'refund',
|
|
1305
1298
|
'/_studio/api/lounge/rules': 'lounge-rules', '/_studio/api/lounge/night': 'lounge-night', '/_studio/api/lounge/mod': 'lounge-mod',
|
|
1306
1299
|
'/_studio/api/lounge/remove': 'lounge-remove', '/_studio/api/lounge/hold': 'lounge-hold',
|
|
1307
1300
|
};
|
package/worker/parts-sale.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { amountError } from './shop-rules.mjs';
|
|
2
2
|
import { digest, manifestHash } from "./purchase-crypto.mjs";
|
|
3
3
|
/** Paid-part metadata shared by the builder, buyer and Worker. No payment policy ceilings. */
|
|
4
|
-
import { canonicalJson } from './
|
|
4
|
+
import { canonicalJson } from './canonical-json.mjs';
|
|
5
5
|
|
|
6
6
|
export const quoteHash = async (part, quantity = 1, game = null, offer = part.sale) => digest(canonicalJson({ manifest: await manifestHash(part), part: part.id, release: part.version, scope: offer.scope, quantity, game, interval: offer.billing, currency: offer.currency, amount: offer.amount * quantity, offer }));
|
|
7
7
|
export const legalTerms = (p) => ({ license: p.license, terms: p.files?.find((f) => f.path === p.licenseTerms)?.sha256, scope: p.sale?.scope, source: p.sale?.source, commercialUse: p.sale?.commercialUse, transferable: p.sale?.transferable, onRefund: p.sale?.onRefund, onExpiry: p.sale?.onExpiry, refund: p.sale?.refund, updates: p.sale?.updates });
|
package/worker/purchase-core.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { canonicalJson } from "./
|
|
1
|
+
import { canonicalJson } from "./canonical-json.mjs";
|
|
2
2
|
export const GRANT_TYPE = 'homie-purchase-grant+jwt';
|
|
3
3
|
export const PROOF_TYPE = 'homie-purchase-proof+jwt';
|
|
4
4
|
export const PROOF_AUDIENCE = 'homie-purchase-evidence';
|
package/worker/purchase-keys.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Public Payment Link configuration, installed by the owner's existing CLI authorization. */
|
|
2
2
|
import { paymentLinkURL, linkConfig } from './shop-links.mjs';
|
|
3
|
-
import { canonicalJson } from './
|
|
3
|
+
import { canonicalJson } from './canonical-json.mjs';
|
|
4
4
|
export function purchaseLinks(env) {
|
|
5
5
|
try { const c = JSON.parse(env.PURCHASE_LINKS ?? 'null'); return c?.v === 1 && c.mode === linkConfig(env)?.mode && Array.isArray(c.offers) ? c : null; } catch { return null; }
|
|
6
6
|
}
|
|
@@ -5,7 +5,7 @@ import { orderFact, recordOrderState } from './purchase-core.mjs';
|
|
|
5
5
|
import { machineCapabilities } from './payment-capabilities.mjs';
|
|
6
6
|
/** Studio licences use the shop's Stripe, order book, webhook and refund office. */
|
|
7
7
|
|
|
8
|
-
import { canonicalJson } from './
|
|
8
|
+
import { canonicalJson } from './canonical-json.mjs';
|
|
9
9
|
|
|
10
10
|
import {
|
|
11
11
|
cancelSubscription,
|
|
@@ -4,7 +4,7 @@ import { hostedReadiness } from './purchase-hosted-readiness.mjs';
|
|
|
4
4
|
import { orderFact, orderState, recordOrderState, offerVersion } from './purchase-core.mjs';
|
|
5
5
|
/** Studio licences use the shop's Stripe, order book, webhook and refund office. */
|
|
6
6
|
|
|
7
|
-
import { canonicalJson } from './
|
|
7
|
+
import { canonicalJson } from './canonical-json.mjs';
|
|
8
8
|
import { signingKey, publicKeys } from './purchase-keys.mjs';
|
|
9
9
|
import {
|
|
10
10
|
createCheckoutSession,
|
|
@@ -3,7 +3,7 @@ import { problem } from './purchase-problem.mjs';
|
|
|
3
3
|
/** Resource purchases share the studio's Stripe webhook and refund office. */
|
|
4
4
|
import { offerVersion } from "./purchase-core.mjs";
|
|
5
5
|
|
|
6
|
-
import { canonicalJson } from "./
|
|
6
|
+
import { canonicalJson } from "./canonical-json.mjs";
|
|
7
7
|
|
|
8
8
|
import { modeOf } from "./stripe.mjs";
|
|
9
9
|
|
package/worker/shop-page.mjs
CHANGED
|
@@ -522,8 +522,8 @@ export const OFFICE_SHOP_SCRIPT = String.raw`(function () {
|
|
|
522
522
|
function cents(n, c) { var scale = zeroDecimal.indexOf(String(c || 'usd').toLowerCase()) >= 0 ? 1 : ['bhd', 'jod', 'kwd', 'omr', 'tnd'].indexOf(String(c).toLowerCase()) >= 0 ? 1000 : 100; try { return new Intl.NumberFormat('en-US', { style: 'currency', currency: String(c || 'usd').toUpperCase(), minimumFractionDigits: scale === 1 || ['isk', 'ugx'].indexOf(c) >= 0 ? 0 : Math.log10(scale), maximumFractionDigits: scale === 1 || ['isk', 'ugx'].indexOf(c) >= 0 ? 0 : Math.log10(scale) }).format(n / scale); } catch (e) { return (n / scale).toFixed(scale === 1 ? 0 : Math.log10(scale)); } }
|
|
523
523
|
function toast(t) { var x = $('#toast'); x.textContent = t; x.hidden = false; clearTimeout(toast.t); toast.t = setTimeout(function () { x.hidden = true; }, 4000); }
|
|
524
524
|
function link(href, text) { var a = el('a', { href: href, target: '_blank', rel: 'noopener' }, text); return a; }
|
|
525
|
-
function load(
|
|
526
|
-
api('GET', '/_studio/api/shop?
|
|
525
|
+
function load(orderCursor) {
|
|
526
|
+
api('GET', '/_studio/api/shop?orderCursor=' + encodeURIComponent(orderCursor || '')).then(function (s) {
|
|
527
527
|
var top = $('#state'); top.textContent = '';
|
|
528
528
|
if (!s.ok) { top.appendChild(el('p', { class: 'empty' }, s.message || 'The shop did not answer.')); return; }
|
|
529
529
|
top.appendChild(el('p', null, s.ready ? ('Open' + (s.mode === 'test' ? ' in TEST MODE (no real money)' : ', live') + (s.shop ? ', till: ' + s.shop.till : '')) : 'Not selling yet.'));
|
|
@@ -542,7 +542,7 @@ export const OFFICE_SHOP_SCRIPT = String.raw`(function () {
|
|
|
542
542
|
var row = el('div', { class: 'row' });
|
|
543
543
|
row.appendChild(el('span', null, new Date(o.createdAt).toISOString().slice(0, 16).replace('T', ' ')));
|
|
544
544
|
row.appendChild(el('b', null, o.name + ' · ' + o.shown));
|
|
545
|
-
row.appendChild(el('span', null, (o.player && o.player.name ? o.player.name : o.resourceKind ? 'a studio purchase' : 'a deleted account') + (o.parent ? ' (a parent paid)' : '')
|
|
545
|
+
row.appendChild(el('span', null, (o.player && o.player.name ? o.player.name : o.resourceKind ? 'a studio purchase' : 'a deleted account') + (o.parent ? ' (a parent paid)' : '')));
|
|
546
546
|
row.appendChild(el('span', { class: 'st st-' + o.status }, o.status + (o.mode === 'test' ? ' · test' : '') + (o.note ? ' · ' + o.note : '')));
|
|
547
547
|
if (o.stripe) row.appendChild(link(o.stripe, 'In Stripe'));
|
|
548
548
|
if (o.dispute) row.appendChild(link(o.dispute, 'The dispute'));
|
|
@@ -578,70 +578,7 @@ export const OFFICE_SHOP_SCRIPT = String.raw`(function () {
|
|
|
578
578
|
});
|
|
579
579
|
ord.appendChild(row);
|
|
580
580
|
});
|
|
581
|
-
if (s.nextOrdersCursor) { var nextOrders = el('button', { type: 'button' }, 'Next orders'); nextOrders.onclick = function () { load(
|
|
582
|
-
var ref = $('#referrals'); ref.textContent = '';
|
|
583
|
-
if (s.referrals.nextCursor) { var next = el('button', { type: 'button' }, 'Next referrers'); next.onclick = function () { load(s.referrals.nextCursor); }; ref.appendChild(next); }
|
|
584
|
-
(s.referrals.failures || []).forEach(function (x) { ref.appendChild(el('p', null, 'Statement failed for ' + x.via + ': ' + (x.why || x.status) + '. Start again after completion to retry.')); });
|
|
585
|
-
var owe = s.referrals.owe || [];
|
|
586
|
-
ref.appendChild(el('h3', null, 'You owe referrers'));
|
|
587
|
-
if (!owe.length) ref.appendChild(el('p', { class: 'dim' }, 'Nothing: no sale came from another site\'s link yet.'));
|
|
588
|
-
owe.forEach(function (b) {
|
|
589
|
-
var detail = el('details'); detail.appendChild(el('summary', null, 'Sales'));
|
|
590
|
-
var sales = el('div'); detail.appendChild(sales);
|
|
591
|
-
var more = el('button', { type: 'button' }, 'Show sales'); detail.appendChild(more);
|
|
592
|
-
var lineCursor = '';
|
|
593
|
-
more.onclick = function () {
|
|
594
|
-
more.disabled = true;
|
|
595
|
-
api('GET', '/_studio/api/shop/lines?via=' + encodeURIComponent(b.via) + '¤cy=' + b.currency + '&cursor=' + encodeURIComponent(lineCursor)).then(function (page) {
|
|
596
|
-
sales.textContent = '';
|
|
597
|
-
page.lines.forEach(function (line) { sales.appendChild(el('p', null, line.date + ' · ' + line.item + ' · ' + cents(line.share, b.currency) + ' · ' + line.state)); });
|
|
598
|
-
lineCursor = page.nextCursor; more.hidden = !lineCursor; more.disabled = false; more.textContent = 'Next sales';
|
|
599
|
-
}).catch(function () { more.disabled = false; toast('Sales did not answer.'); });
|
|
600
|
-
};
|
|
601
|
-
var row = el('div', { class: 'row' });
|
|
602
|
-
row.appendChild(el('b', null, b.via));
|
|
603
|
-
row.appendChild(el('span', null, 'due ' + cents(b.due, b.currency) + ' · held ' + cents(b.pending, b.currency) + ' · ' + b.lineCount + ' sale(s)'));
|
|
604
|
-
if (b.due > 0) {
|
|
605
|
-
var m = el('button', { type: 'button', class: 'ghost small' }, 'Mark paid');
|
|
606
|
-
m.addEventListener('click', function () {
|
|
607
|
-
var ref0 = prompt('What paid it (an invoice number, a PayPal reference)?'); if (ref0 === null) return;
|
|
608
|
-
api('POST', '/_studio/api/shop/settle', { via: b.via, currency: b.currency, ref: ref0 }).then(function (r) { if (!r.ok) throw new Error(r.message); toast('Marked paid.'); load(); }).catch(function (e) { toast(e.message); });
|
|
609
|
-
});
|
|
610
|
-
row.appendChild(m);
|
|
611
|
-
}
|
|
612
|
-
ref.appendChild(row); ref.appendChild(detail);
|
|
613
|
-
});
|
|
614
|
-
var send = el('button', { type: 'button', class: 'ghost small' }, 'Send last month\'s signed statements');
|
|
615
|
-
send.addEventListener('click', async function () {
|
|
616
|
-
send.disabled = true; send.textContent = 'Sending; keep this tab open. Close it and press again to resume safely.';
|
|
617
|
-
var cursor = '', period = null, sent = 0, failed = [];
|
|
618
|
-
try {
|
|
619
|
-
do {
|
|
620
|
-
var r = await api('POST', '/_studio/api/shop/statements/send', { cursor: cursor, period: period });
|
|
621
|
-
if (!r.ok) throw new Error(r.message || 'Not sent.');
|
|
622
|
-
sent += r.sent.filter(function (x) { return x.ok; }).length;
|
|
623
|
-
failed = failed.concat(r.sent.filter(function (x) { return !x.ok; }).map(function (x) { return x.via + ': ' + (x.why || x.status); }));
|
|
624
|
-
send.textContent = 'Sent ' + sent + ' pages. Keep open; press again after closing to resume safely.';
|
|
625
|
-
cursor = r.nextCursor; period = r.period;
|
|
626
|
-
if (r.retryAfter) { send.textContent = 'Retrying with backoff; keep open, or press again later to resume safely.'; await new Promise(function (resolve) { setTimeout(resolve, r.retryAfter * 1000); }); }
|
|
627
|
-
} while (cursor !== null);
|
|
628
|
-
if (r.failures) failed = r.failures.map(function (x) { return x.via + ': ' + (x.why || x.status); });
|
|
629
|
-
toast('Sent ' + sent + ' statement pages; ' + failed.length + ' failed.' + (failed.length ? ' ' + failed.join('; ') : ''));
|
|
630
|
-
} catch (e) { toast(e.message); }
|
|
631
|
-
send.disabled = false; send.textContent = 'Send last month’s signed statements';
|
|
632
|
-
});
|
|
633
|
-
ref.appendChild(send);
|
|
634
|
-
ref.appendChild(el('h3', null, 'Owed to you'));
|
|
635
|
-
var inn = s.referrals.owedToUs || [];
|
|
636
|
-
if (!inn.length) ref.appendChild(el('p', { class: 'dim' }, 'No statements yet. When another studio sells to a player you sent, it sends a signed statement here.'));
|
|
637
|
-
inn.forEach(function (x) {
|
|
638
|
-
var row = el('div', { class: 'row' });
|
|
639
|
-
row.appendChild(el('b', null, x.seller.replace(/^https:\/\//, '')));
|
|
640
|
-
row.appendChild(el('span', null, x.period + ': due ' + cents(x.due, x.currency) + ', held ' + cents(x.pending, x.currency)));
|
|
641
|
-
row.appendChild(link('/_studio/api/shop/received?seller=' + encodeURIComponent(x.seller) + '&period=' + encodeURIComponent(x.period), 'Signed statement pages'));
|
|
642
|
-
if (x.due > 0) row.appendChild(link(s.referrals.invoice, 'Invoice them in Stripe'));
|
|
643
|
-
ref.appendChild(row);
|
|
644
|
-
});
|
|
581
|
+
if (s.nextOrdersCursor) { var nextOrders = el('button', { type: 'button' }, 'Next orders'); nextOrders.onclick = function () { load(s.nextOrdersCursor); }; ord.appendChild(nextOrders); }
|
|
645
582
|
}).catch(function () { $('#state').textContent = 'The shop did not answer.'; });
|
|
646
583
|
}
|
|
647
584
|
load();
|
|
@@ -661,9 +598,8 @@ export async function officeShopPage(cat) {
|
|
|
661
598
|
<style>.row{display:flex;flex-wrap:wrap;gap:6px 14px;align-items:center;padding:10px 0;border-top:1px solid var(--line)}.row b{font-weight:700}.st{font-size:12px;padding:2px 8px;border-radius:999px;border:1px solid var(--line2)}.st-refunded,.st-lost{opacity:.7}.st-disputed{border-color:#ff8a80;color:#ff8a80}h3{margin:22px 0 6px;font-size:15px}.links a{margin-right:2px}</style>
|
|
662
599
|
<div class="game" style="padding:14px 16px" id="state"><p class="empty">Reading the shop…</p></div>
|
|
663
600
|
<div class="game" style="padding:6px 16px 12px"><h3>Orders</h3><div id="orders"></div></div>
|
|
664
|
-
<div class="game" style="padding:6px 16px 12px" id="referrals"></div>
|
|
665
601
|
<div class="toast" id="toast" role="status" hidden></div>
|
|
666
|
-
<p class="note">You are the seller. Money goes from players to your own Stripe account; Homie never sees it and takes nothing. Refunds are handled in Stripe or by your connected AI; shops with the fuller checkout connection also offer Refund here. A card dispute never touches the player's account: answer it in Stripe; if you lose it, that one item is taken back. Tax: when enabled, Stripe Tax works out and collects tax where you told Stripe you are registered${cat.shop?.till === 'stripe-managed' ? '; with Managed Payments Stripe is the seller of record and files it for you' : ''}. Payouts, the balance and receipts are Stripe's own pages, linked above.
|
|
602
|
+
<p class="note">You are the seller. Money goes from players to your own Stripe account; Homie never sees it and takes nothing. Refunds are handled in Stripe or by your connected AI; shops with the fuller checkout connection also offer Refund here. A card dispute never touches the player's account: answer it in Stripe; if you lose it, that one item is taken back. Tax: when enabled, Stripe Tax works out and collects tax where you told Stripe you are registered${cat.shop?.till === 'stripe-managed' ? '; with Managed Payments Stripe is the seller of record and files it for you' : ''}. Payouts, the balance and receipts are Stripe's own pages, linked above. This is not legal or tax advice.</p>`;
|
|
667
603
|
return new Response(shell(cat, `Shop · ${name}`, body, { script }), {
|
|
668
604
|
headers: { ...PRIVATE, 'content-security-policy': `default-src 'none'; style-src 'unsafe-inline'; img-src data:; script-src '${officeHash}'; connect-src 'self'; form-action 'self'; frame-ancestors 'none'; base-uri 'none'` },
|
|
669
605
|
});
|
package/worker/shop-rules.mjs
CHANGED
|
@@ -47,18 +47,6 @@ export function amountError(amount, currency) {
|
|
|
47
47
|
return null;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
/** Round a decimal rate times integer money exactly, including exponent notation. */
|
|
51
|
-
export function referralShare(amount, rate) {
|
|
52
|
-
const [digits, exponent = '0'] = String(rate).toLowerCase().split('e');
|
|
53
|
-
const [whole, fraction = ''] = digits.split('.');
|
|
54
|
-
const scale = fraction.length - Number(exponent);
|
|
55
|
-
let numerator = BigInt(whole + fraction), denominator = 1n;
|
|
56
|
-
if (scale > 0) denominator = 10n ** BigInt(scale); else numerator *= 10n ** BigInt(-scale);
|
|
57
|
-
const share = Number((2n * BigInt(amount) * numerator + denominator) / (2n * denominator));
|
|
58
|
-
if (!Number.isSafeInteger(share)) throw new RangeError('Referral share exceeds safe integer arithmetic');
|
|
59
|
-
return share;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
50
|
/** Checkout wording is shared by validation and the request path. */
|
|
63
51
|
export function checkoutMessage(shop, studio = 'this studio', parent = false) {
|
|
64
52
|
return `${parent ? "This is for your child's account. " : ''}Delivered at once to the player's account on ${studio}. ${shop.policy.withdrawalAcknowledgement ? 'By paying you ask for it now, which ends the 14-day withdrawal right;' : 'Your statutory rights still apply.'} ${shop.refundDays === null ? (shop.policy.withdrawalAcknowledgement ? 'ask the studio about refunds.' : 'Ask the studio about refunds.') : `${shop.policy.refundUsedItems ? 'an item' : 'an unused item'} can still be refunded within ${shop.refundDays} days.`}`;
|
|
@@ -138,7 +126,7 @@ export function checkShop(raw, { games = null, audience = 'general', studioName
|
|
|
138
126
|
const purchaseAttemptsPerMinute = ratePolicy('purchaseAttemptsPerMinute');
|
|
139
127
|
const purchaseAttemptsPerAddressPerMinute = ratePolicy('purchaseAttemptsPerAddressPerMinute');
|
|
140
128
|
|
|
141
|
-
|
|
129
|
+
if (raw.automaticTax !== undefined && typeof raw.automaticTax !== 'boolean') err('automaticTax', 'use a boolean');
|
|
142
130
|
const items = [];
|
|
143
131
|
const list = Array.isArray(raw.items) ? raw.items : [];
|
|
144
132
|
if (raw.items !== undefined && !Array.isArray(raw.items)) err('items', 'items is a list');
|
|
@@ -209,24 +197,8 @@ export function checkShop(raw, { games = null, audience = 'general', studioName
|
|
|
209
197
|
});
|
|
210
198
|
});
|
|
211
199
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const r = raw.referrals;
|
|
215
|
-
if (!isObj(r)) err('referrals', 'referrals is { "rate": 0.10, "windowDays": 30, "capPerPlayer": 1000, "holdDays": 30, "minimumInvoice": 2500 } (or leave it out: nobody is paid for sending players)');
|
|
216
|
-
else {
|
|
217
|
-
const rate = optionalNumber(r.rate ?? 0, 'referrals.rate', { integer: false });
|
|
218
|
-
const windowDays = optionalNumber(r.windowDays, 'referrals.windowDays');
|
|
219
|
-
const capPerPlayer = optionalNumber(r.capPerPlayer, 'referrals.capPerPlayer');
|
|
220
|
-
const holdDays = optionalNumber(r.holdDays, 'referrals.holdDays');
|
|
221
|
-
const minimumInvoice = optionalNumber(r.minimumInvoice, 'referrals.minimumInvoice');
|
|
222
|
-
const accept = Array.isArray(r.accept) ? r.accept.map(String) : [];
|
|
223
|
-
const billing = r.billingEmail === undefined ? null : String(r.billingEmail);
|
|
224
|
-
if (billing !== null && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(billing)) err('referrals.billingEmail', 'billingEmail is where referrers send their invoices (an address the studio reads)');
|
|
225
|
-
referrals = { rate, windowDays, capPerPlayer, holdDays, minimumInvoice, basis: 'pre-tax', accept, ...(billing ? { billingEmail: billing } : {}) };
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// Where `homie-studio shop catalog` made the items' Products in Stripe (0.24.3): the Worker's checkouts then name them.
|
|
200
|
+
for (const key of ['referrals', 'referralNewPlayersOnly']) if (Object.hasOwn(raw, key)) warnings.push({ at: key, message: 'ignored; Homie has no referral system.' });
|
|
201
|
+
// Where `homie-studio shop catalog` made the items' Products in Stripe: checkouts then name them.
|
|
230
202
|
let catalog = [];
|
|
231
203
|
if (raw.catalog !== undefined && raw.catalog !== null) {
|
|
232
204
|
if (!Array.isArray(raw.catalog) || raw.catalog.some((m) => m !== 'test' && m !== 'live')) err('catalog', 'catalog lists where the items are Products in Stripe: ["test"], ["test", "live"] (homie-studio shop catalog writes it)');
|
|
@@ -237,7 +209,7 @@ export function checkShop(raw, { games = null, audience = 'general', studioName
|
|
|
237
209
|
const finalTill = kids ? 'off' : TILLS.includes(till) ? till : 'off';
|
|
238
210
|
if (kids && till !== 'off') warnings.push({ at: 'till', message: 'studio.json says "audience": "kids": the studio sells nothing in its games (the till is off under policy.kidsStudio).' });
|
|
239
211
|
const shop = {
|
|
240
|
-
v: 1, ...(isObj(raw.giving) ? { giving: { receiptText: String(raw.giving.receiptText ?? '') } } : {}), policy, purchasesTill: TILLS.includes(raw.purchasesTill ?? till) ? (raw.purchasesTill ?? till) : 'off', ...(/^https:\/\/billing\.stripe\.com\/p\/login\/[A-Za-z0-9_]+$/.test(raw.purchasesPortalLogin ?? '') ? { purchasesPortalLogin: raw.purchasesPortalLogin } : {}), checkoutMinutes, requestBytes, automaticTax: raw.automaticTax === true,
|
|
212
|
+
v: 1, ...(isObj(raw.giving) ? { giving: { receiptText: String(raw.giving.receiptText ?? '') } } : {}), policy, purchasesTill: TILLS.includes(raw.purchasesTill ?? till) ? (raw.purchasesTill ?? till) : 'off', ...(/^https:\/\/billing\.stripe\.com\/p\/login\/[A-Za-z0-9_]+$/.test(raw.purchasesPortalLogin ?? '') ? { purchasesPortalLogin: raw.purchasesPortalLogin } : {}), checkoutMinutes, requestBytes, automaticTax: raw.automaticTax === true, guestBuyersPerAddressPerHour, purchaseAttemptsPerMinute, purchaseAttemptsPerAddressPerMinute, till: finalTill, currency: CURRENCY.test(currency) ? currency : 'usd', refundDays, capPerPlayerMonth: cap, items,
|
|
241
213
|
open: finalTill !== 'off' && items.length > 0, ...(kids ? { audience: 'kids' } : {}), ...(catalog.length ? { catalog } : {}),
|
|
242
214
|
};
|
|
243
215
|
// https://docs.stripe.com/api/checkout/sessions/create#create_checkout_session-custom_text-submit-message
|
|
@@ -305,7 +277,7 @@ export function wayFor(item, { open = true, kids = false, beginner = false, play
|
|
|
305
277
|
/** The public face of a shop (for the directory manifest and the shop page): never a secret, never a person. */
|
|
306
278
|
export function publicShop(shop) {
|
|
307
279
|
if (!shop) return null;
|
|
308
|
-
return { policy: shop.policy, till: shop.till, currency: shop.currency, refundDays: shop.refundDays, capPerPlayerMonth: shop.capPerPlayerMonth, open: Boolean(shop.open), items: shop.items.length
|
|
280
|
+
return { policy: shop.policy, till: shop.till, currency: shop.currency, refundDays: shop.refundDays, capPerPlayerMonth: shop.capPerPlayerMonth, open: Boolean(shop.open), items: shop.items.length };
|
|
309
281
|
}
|
|
310
282
|
|
|
311
283
|
/** A price for people: 500 usd → "$5.00", 300 eur → "€3.00", 1250 cad → "CA$12.50". */
|
package/worker/shop-store.mjs
CHANGED
|
@@ -3,10 +3,9 @@
|
|
|
3
3
|
* from worker/shop.mjs so player accounts (worker/players.mjs) can export and forget a player's purchases without
|
|
4
4
|
* importing the routes.
|
|
5
5
|
*
|
|
6
|
-
* WHAT IS STORED: orders (the item, the price, the state, Stripe's ids
|
|
6
|
+
* WHAT IS STORED: orders (the item, the price, the state, Stripe's ids), entitlements (what a
|
|
7
7
|
* player owns, by the keys the game reads), the age question's answer as a BAND only (adult, teen or child; never a
|
|
8
|
-
* date), one-time links a teenager hands a parent, the webhook events already handled (ids only),
|
|
9
|
-
* (the seller's books) and the statements other studios sent this one. NO card, NO address, NO email, NO name:
|
|
8
|
+
* date), one-time links a teenager hands a parent, the webhook events already handled (ids only). NO card, NO address, NO email, NO name:
|
|
10
9
|
* Stripe keeps the buyer's details and sends the receipt.
|
|
11
10
|
*/
|
|
12
11
|
|
|
@@ -31,7 +30,6 @@ CREATE TABLE IF NOT EXISTS shop_orders (
|
|
|
31
30
|
refund TEXT,
|
|
32
31
|
dispute TEXT,
|
|
33
32
|
parent INTEGER NOT NULL DEFAULT 0,
|
|
34
|
-
via TEXT,
|
|
35
33
|
note TEXT,
|
|
36
34
|
created_at INTEGER NOT NULL,
|
|
37
35
|
paid_at INTEGER,
|
|
@@ -71,30 +69,6 @@ CREATE TABLE IF NOT EXISTS shop_parent_links (
|
|
|
71
69
|
order_id TEXT,
|
|
72
70
|
expires_at INTEGER NOT NULL
|
|
73
71
|
) WITHOUT ROWID;
|
|
74
|
-
CREATE TABLE IF NOT EXISTS referral_lines (
|
|
75
|
-
order_id TEXT PRIMARY KEY,
|
|
76
|
-
via TEXT NOT NULL,
|
|
77
|
-
net INTEGER NOT NULL,
|
|
78
|
-
rate REAL NOT NULL,
|
|
79
|
-
share INTEGER NOT NULL,
|
|
80
|
-
currency TEXT NOT NULL,
|
|
81
|
-
state TEXT NOT NULL,
|
|
82
|
-
period TEXT NOT NULL,
|
|
83
|
-
hold_until INTEGER NOT NULL,
|
|
84
|
-
settled_ref TEXT,
|
|
85
|
-
created_at INTEGER NOT NULL
|
|
86
|
-
) WITHOUT ROWID;
|
|
87
|
-
CREATE INDEX IF NOT EXISTS referral_lines_via ON referral_lines (via, period);
|
|
88
|
-
CREATE TABLE IF NOT EXISTS referral_statements (
|
|
89
|
-
seller TEXT NOT NULL,
|
|
90
|
-
period TEXT NOT NULL,
|
|
91
|
-
owed INTEGER NOT NULL,
|
|
92
|
-
pending INTEGER NOT NULL,
|
|
93
|
-
currency TEXT NOT NULL,
|
|
94
|
-
body TEXT NOT NULL,
|
|
95
|
-
received_at INTEGER NOT NULL,
|
|
96
|
-
PRIMARY KEY (seller, period)
|
|
97
|
-
) WITHOUT ROWID;
|
|
98
72
|
`;
|
|
99
73
|
|
|
100
74
|
export const SHOP_RESERVATIONS_FILE = '0010_shop_reservations.sql';
|
|
@@ -102,26 +76,6 @@ export const SHOP_RESERVATIONS = `-- Checkout expiry in milliseconds; only sessi
|
|
|
102
76
|
ALTER TABLE shop_orders ADD COLUMN expires_at INTEGER;
|
|
103
77
|
-- Previously opened sessions used the same explicit 31-minute lifetime.
|
|
104
78
|
UPDATE shop_orders SET expires_at = (CAST(created_at / 1000 AS INTEGER) + 31 * 60) * 1000 WHERE status IN ('started', 'processing');
|
|
105
|
-
CREATE INDEX IF NOT EXISTS referral_lines_book ON referral_lines (via, currency, order_id);
|
|
106
|
-
`;
|
|
107
|
-
|
|
108
|
-
export const SHOP_STATEMENTS_FILE = '0011_shop_statements.sql';
|
|
109
|
-
export const SHOP_STATEMENTS = `-- Statements retain independent currencies and an immutable edition while sending.
|
|
110
|
-
ALTER TABLE referral_statements RENAME TO referral_statements_old;
|
|
111
|
-
CREATE TABLE referral_statements (
|
|
112
|
-
seller TEXT NOT NULL, period TEXT NOT NULL, owed INTEGER NOT NULL, pending INTEGER NOT NULL,
|
|
113
|
-
currency TEXT NOT NULL, body TEXT NOT NULL, received_at INTEGER NOT NULL,
|
|
114
|
-
PRIMARY KEY (seller, period, currency)
|
|
115
|
-
) WITHOUT ROWID;
|
|
116
|
-
INSERT INTO referral_statements SELECT * FROM referral_statements_old;
|
|
117
|
-
DROP TABLE referral_statements_old;
|
|
118
|
-
CREATE TABLE referral_editions (id TEXT PRIMARY KEY, period TEXT NOT NULL, issued INTEGER NOT NULL) WITHOUT ROWID;
|
|
119
|
-
CREATE TABLE referral_edition_lines (
|
|
120
|
-
edition TEXT NOT NULL, order_id TEXT NOT NULL, via TEXT NOT NULL, currency TEXT NOT NULL,
|
|
121
|
-
net INTEGER NOT NULL, rate REAL NOT NULL, share INTEGER NOT NULL, state TEXT NOT NULL,
|
|
122
|
-
period TEXT NOT NULL, hold_until INTEGER NOT NULL, created_at INTEGER NOT NULL, item TEXT, paid_at INTEGER,
|
|
123
|
-
PRIMARY KEY (edition, via, currency, order_id)
|
|
124
|
-
) WITHOUT ROWID;
|
|
125
79
|
`;
|
|
126
80
|
|
|
127
81
|
export const SHOP_LINES_FILE = '0012_shop_lines.sql';
|
|
@@ -150,7 +104,6 @@ END;
|
|
|
150
104
|
UPDATE shop_order_lines SET refunded_amount = COALESCE(total, amount), refunded_net = amount WHERE status = 'refunded';
|
|
151
105
|
ALTER TABLE shop_parent_links ADD COLUMN cart TEXT;
|
|
152
106
|
ALTER TABLE shop_orders ADD COLUMN whole_refund TEXT;
|
|
153
|
-
ALTER TABLE shop_orders ADD COLUMN referral_terms TEXT;
|
|
154
107
|
ALTER TABLE shop_orders ADD COLUMN checkout_player TEXT;
|
|
155
108
|
UPDATE shop_orders SET checkout_player = player;
|
|
156
109
|
CREATE TRIGGER shop_checkout_player_insert AFTER INSERT ON shop_orders BEGIN
|
|
@@ -159,8 +112,6 @@ END;
|
|
|
159
112
|
CREATE TRIGGER shop_forget_checkout_player AFTER UPDATE OF player ON shop_orders WHEN NEW.player IS NULL BEGIN
|
|
160
113
|
UPDATE shop_orders SET checkout_player = NULL WHERE id = NEW.id;
|
|
161
114
|
END;
|
|
162
|
-
ALTER TABLE referral_lines ADD COLUMN original_share INTEGER;
|
|
163
|
-
UPDATE referral_lines SET original_share = share;
|
|
164
115
|
ALTER TABLE shop_orders ADD COLUMN attention INTEGER NOT NULL DEFAULT 1;
|
|
165
116
|
UPDATE shop_orders SET attention = 0 WHERE status = 'paid' AND EXISTS (SELECT 1 FROM entitlements WHERE order_id = shop_orders.id);
|
|
166
117
|
ALTER TABLE shop_orders ADD COLUMN refunded_amount INTEGER NOT NULL DEFAULT 0;
|
|
@@ -190,13 +141,10 @@ export async function migrationNeeded(env) {
|
|
|
190
141
|
for (const [file, query] of [
|
|
191
142
|
[SHOP_MIGRATION_FILE, 'SELECT 1 FROM shop_orders LIMIT 1'],
|
|
192
143
|
[SHOP_RESERVATIONS_FILE, 'SELECT expires_at FROM shop_orders LIMIT 1'],
|
|
193
|
-
[SHOP_STATEMENTS_FILE, 'SELECT issued FROM referral_editions LIMIT 1'],
|
|
194
|
-
[SHOP_STATEMENTS_FILE, 'SELECT edition FROM referral_edition_lines LIMIT 1'],
|
|
195
144
|
[SHOP_LINES_FILE, 'SELECT snapshot, total, refunded_amount FROM shop_order_lines LIMIT 1'],
|
|
196
145
|
[SHOP_LINES_FILE, 'SELECT line_id, quantity FROM shop_entitlement_lines LIMIT 1'],
|
|
197
|
-
[SHOP_LINES_FILE, 'SELECT
|
|
146
|
+
[SHOP_LINES_FILE, 'SELECT checkout_player, attention, refunded_amount, refund_revision FROM shop_orders LIMIT 1'],
|
|
198
147
|
[SHOP_LINES_FILE, 'SELECT cart FROM shop_parent_links LIMIT 1'],
|
|
199
|
-
[SHOP_LINES_FILE, 'SELECT original_share FROM referral_lines LIMIT 1'],
|
|
200
148
|
]) {
|
|
201
149
|
try { await env.DB.prepare(query).first(); } catch { return file; }
|
|
202
150
|
}
|
|
@@ -296,7 +244,7 @@ export const orderByPayment = (env, payment) => env.DB.prepare('SELECT * FROM sh
|
|
|
296
244
|
export function orderView(r) {
|
|
297
245
|
return {
|
|
298
246
|
id: r.id, refundedAmount: Number(r.refunded_amount ?? 0), item: r.item, game: r.game ?? null, amount: Number(r.amount), currency: r.currency, tax: r.tax === null ? null : Number(r.tax), total: r.total === null ? null : Number(r.total),
|
|
299
|
-
status: r.status, till: r.till, mode: r.mode, parent: Number(r.parent) === 1,
|
|
247
|
+
status: r.status, till: r.till, mode: r.mode, parent: Number(r.parent) === 1,
|
|
300
248
|
createdAt: Number(r.created_at), paidAt: r.paid_at === null ? null : Number(r.paid_at), refundedAt: r.refunded_at === null ? null : Number(r.refunded_at),
|
|
301
249
|
};
|
|
302
250
|
}
|
|
@@ -368,3 +316,75 @@ export async function adoptShopStatements(env, guest, player) {
|
|
|
368
316
|
env.DB.prepare('DELETE FROM player_age WHERE player = ?1').bind(guest),
|
|
369
317
|
];
|
|
370
318
|
}
|
|
319
|
+
|
|
320
|
+
export const SHOP_REMOVE_REFERRALS_FILE = '0022_remove_referrals.sql';
|
|
321
|
+
export const SHOP_REMOVE_REFERRALS = `-- Remove referral bookkeeping; no studio we run has data in these tables.
|
|
322
|
+
DROP TABLE IF EXISTS referral_lines;
|
|
323
|
+
DROP TABLE IF EXISTS referral_statements;
|
|
324
|
+
DROP TABLE IF EXISTS referral_statements_old;
|
|
325
|
+
DROP TABLE IF EXISTS referral_editions;
|
|
326
|
+
DROP TABLE IF EXISTS referral_edition_lines;
|
|
327
|
+
DELETE FROM meta WHERE key IN ('referral_cookie_key', 'referral_signing_key') OR key GLOB 'referral-*';
|
|
328
|
+
UPDATE shop_parent_links SET cart = json_remove(cart, '$[0].via') WHERE json_valid(cart);
|
|
329
|
+
-- Rebuild orders to remove sales attribution, on both old and fresh schemas.
|
|
330
|
+
CREATE TABLE shop_orders_clean (
|
|
331
|
+
id TEXT PRIMARY KEY,
|
|
332
|
+
player TEXT,
|
|
333
|
+
item TEXT NOT NULL,
|
|
334
|
+
game TEXT,
|
|
335
|
+
amount INTEGER NOT NULL,
|
|
336
|
+
currency TEXT NOT NULL,
|
|
337
|
+
tax INTEGER,
|
|
338
|
+
total INTEGER,
|
|
339
|
+
till TEXT NOT NULL,
|
|
340
|
+
mode TEXT NOT NULL,
|
|
341
|
+
status TEXT NOT NULL,
|
|
342
|
+
session TEXT UNIQUE,
|
|
343
|
+
payment TEXT,
|
|
344
|
+
refund TEXT,
|
|
345
|
+
dispute TEXT,
|
|
346
|
+
parent INTEGER NOT NULL DEFAULT 0,
|
|
347
|
+
note TEXT,
|
|
348
|
+
created_at INTEGER NOT NULL,
|
|
349
|
+
paid_at INTEGER,
|
|
350
|
+
refunded_at INTEGER,
|
|
351
|
+
updated_at INTEGER NOT NULL,
|
|
352
|
+
expires_at INTEGER,
|
|
353
|
+
whole_refund TEXT,
|
|
354
|
+
checkout_player TEXT,
|
|
355
|
+
attention INTEGER NOT NULL DEFAULT 1,
|
|
356
|
+
refunded_amount INTEGER NOT NULL DEFAULT 0,
|
|
357
|
+
refunded_net INTEGER NOT NULL DEFAULT 0,
|
|
358
|
+
refund_revision INTEGER NOT NULL DEFAULT 0
|
|
359
|
+
);
|
|
360
|
+
INSERT INTO shop_orders_clean (id, player, item, game, amount, currency, tax, total, till, mode, status, session, payment, refund, dispute, parent, note, created_at, paid_at, refunded_at, updated_at, expires_at, whole_refund, checkout_player, attention, refunded_amount, refunded_net, refund_revision) SELECT id, player, item, game, amount, currency, tax, total, till, mode, status, session, payment, refund, dispute, parent, note, created_at, paid_at, refunded_at, updated_at, expires_at, whole_refund, checkout_player, attention, refunded_amount, refunded_net, refund_revision FROM shop_orders;
|
|
361
|
+
DROP TABLE shop_orders;
|
|
362
|
+
ALTER TABLE shop_orders_clean RENAME TO shop_orders;
|
|
363
|
+
CREATE INDEX IF NOT EXISTS shop_orders_player ON shop_orders (player, created_at);
|
|
364
|
+
CREATE INDEX IF NOT EXISTS shop_orders_status ON shop_orders (status, created_at);
|
|
365
|
+
CREATE INDEX IF NOT EXISTS shop_orders_payment ON shop_orders (payment);
|
|
366
|
+
CREATE INDEX shop_orders_attention_player ON shop_orders (mode, player, status, updated_at, id) WHERE attention = 1;
|
|
367
|
+
CREATE INDEX shop_orders_attention ON shop_orders (mode, status, updated_at, id) WHERE attention = 1;
|
|
368
|
+
CREATE TRIGGER shop_checkout_player_insert AFTER INSERT ON shop_orders BEGIN
|
|
369
|
+
UPDATE shop_orders SET checkout_player = NEW.player WHERE id = NEW.id;
|
|
370
|
+
END;
|
|
371
|
+
CREATE TRIGGER shop_forget_checkout_player AFTER UPDATE OF player ON shop_orders WHEN NEW.player IS NULL BEGIN
|
|
372
|
+
UPDATE shop_orders SET checkout_player = NULL WHERE id = NEW.id;
|
|
373
|
+
END;
|
|
374
|
+
-- Restore durable paid and refunded events after rebuilding the order table.
|
|
375
|
+
|
|
376
|
+
DROP TRIGGER IF EXISTS shop_orders_function_paid;
|
|
377
|
+
CREATE TRIGGER shop_orders_function_paid AFTER UPDATE ON shop_orders
|
|
378
|
+
WHEN EXISTS (SELECT 1 FROM function_subscriptions WHERE type='order.paid')
|
|
379
|
+
AND OLD.paid_at IS NULL AND NEW.paid_at IS NOT NULL
|
|
380
|
+
AND NOT EXISTS (SELECT 1 FROM purchase_orders WHERE id=NEW.id) BEGIN
|
|
381
|
+
INSERT OR IGNORE INTO studio_events(id,type,data,at)
|
|
382
|
+
VALUES('shop_orders:' || NEW.id || ':paid','order.paid',
|
|
383
|
+
json_object('order',NEW.id,'source','shop_orders','amount',NEW.amount,'currency',NEW.currency,'mode',NEW.mode,
|
|
384
|
+
'app',NEW.game,'reference',(SELECT value FROM meta WHERE key='shop-reference:' || NEW.id)),NEW.paid_at);
|
|
385
|
+
END;
|
|
386
|
+
|
|
387
|
+
CREATE TRIGGER shop_orders_function_refund AFTER UPDATE ON shop_orders WHEN EXISTS (SELECT 1 FROM function_subscriptions WHERE type='order.refunded') AND OLD.status != 'refunded' AND NEW.status = 'refunded' AND NOT EXISTS (SELECT 1 FROM purchase_orders WHERE id=NEW.id) BEGIN
|
|
388
|
+
INSERT OR IGNORE INTO studio_events(id,type,data,at) VALUES('shop_orders:' || NEW.id || ':refund:' || COALESCE(NEW.refund,''),'order.refunded',json_object('order',NEW.id,'source','shop_orders','refund',NEW.refund,'mode',NEW.mode),NEW.updated_at);
|
|
389
|
+
END;
|
|
390
|
+
`;
|