mcp-scraper 0.35.1 → 0.37.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 +9 -9
- package/dist/bin/api-server.cjs +25071 -19082
- package/dist/bin/api-server.cjs.map +1 -1
- package/dist/bin/api-server.js +3 -3
- package/dist/bin/mcp-scraper-cli.cjs +51 -7
- package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
- package/dist/bin/mcp-scraper-cli.js +48 -5
- package/dist/bin/mcp-scraper-cli.js.map +1 -1
- package/dist/bin/mcp-scraper-install.cjs +2 -2
- package/dist/bin/mcp-scraper-install.cjs.map +1 -1
- package/dist/bin/mcp-scraper-install.js +2 -2
- package/dist/bin/mcp-stdio-server.cjs +995 -222
- package/dist/bin/mcp-stdio-server.cjs.map +1 -1
- package/dist/bin/mcp-stdio-server.js +8 -8
- package/dist/bin/paa-harvest.cjs +125 -70
- package/dist/bin/paa-harvest.cjs.map +1 -1
- package/dist/bin/paa-harvest.js +4 -4
- package/dist/chunk-345BQXZH.js +712 -0
- package/dist/chunk-345BQXZH.js.map +1 -0
- package/dist/chunk-3LWYPAU5.js +7 -0
- package/dist/chunk-3LWYPAU5.js.map +1 -0
- package/dist/{chunk-M2S27J6Z.js → chunk-44HZLHDV.js} +10 -1
- package/dist/chunk-44HZLHDV.js.map +1 -0
- package/dist/{chunk-62DQAWPF.js → chunk-CSCD2HNS.js} +498 -43
- package/dist/chunk-CSCD2HNS.js.map +1 -0
- package/dist/{chunk-ZID3WQID.js → chunk-FQI5PFE7.js} +9 -71
- package/dist/chunk-FQI5PFE7.js.map +1 -0
- package/dist/{chunk-3HBPKR5G.js → chunk-FSAXLDB3.js} +3 -3
- package/dist/chunk-G3P3ZDB4.js +69 -0
- package/dist/chunk-G3P3ZDB4.js.map +1 -0
- package/dist/{chunk-YRGSEY5L.js → chunk-G7KAVJ3F.js} +2 -2
- package/dist/{chunk-YRGSEY5L.js.map → chunk-G7KAVJ3F.js.map} +1 -1
- package/dist/{chunk-NPMW5HUS.js → chunk-JK2FRDAP.js} +930 -244
- package/dist/chunk-JK2FRDAP.js.map +1 -0
- package/dist/{chunk-XVVNKASZ.js → chunk-LOPKN3YL.js} +118 -73
- package/dist/chunk-LOPKN3YL.js.map +1 -0
- package/dist/{chunk-4ZB3X6BQ.js → chunk-MA5JBAUZ.js} +16 -2
- package/dist/{chunk-4ZB3X6BQ.js.map → chunk-MA5JBAUZ.js.map} +1 -1
- package/dist/chunk-PUJFYJXB.js +684 -0
- package/dist/chunk-PUJFYJXB.js.map +1 -0
- package/dist/{chunk-BWXLTWF7.js → chunk-PWPUKR5U.js} +9 -5
- package/dist/chunk-PWPUKR5U.js.map +1 -0
- package/dist/chunk-Q35WZJJK.js +499 -0
- package/dist/chunk-Q35WZJJK.js.map +1 -0
- package/dist/chunk-QZXKQB7Y.js +414 -0
- package/dist/chunk-QZXKQB7Y.js.map +1 -0
- package/dist/{db-YAI5AQOI.js → db-N3YECFWR.js} +12 -2
- package/dist/{extract-bundle-ONWZVV55.js → extract-bundle-346R6MXD.js} +284 -98
- package/dist/extract-bundle-346R6MXD.js.map +1 -0
- package/dist/index.cjs +129 -70
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +4 -4
- package/dist/location-data-repository-O2VII3ON.js +35 -0
- package/dist/{server-GKUTC73B.js → server-QKDDEBVQ.js} +7325 -3762
- package/dist/server-QKDDEBVQ.js.map +1 -0
- package/dist/site-extract-repository-I3VM6WXN.js +62 -0
- package/dist/site-extract-repository-I3VM6WXN.js.map +1 -0
- package/dist/{worker-645BZPEK.js → worker-TTFXPPDK.js} +7 -7
- package/docs/hosted-location-data.md +108 -0
- package/docs/mcp-tool-craft-lint.generated.md +6 -3
- package/docs/mcp-tool-manifest.generated.json +1447 -240
- package/docs/mcp-tool-quality-spec.md +1 -1
- package/docs/specs/connected-services-control-plane-decoupling-spec.md +1044 -0
- package/docs/specs/kernel-stealth-captcha-test-matrix.md +278 -0
- package/docs/specs/multimodal-image-memory-architecture-spec.md +1022 -0
- package/docs/specs/unified-credit-and-scheduled-execution-billing-spec.md +36 -27
- package/package.json +7 -6
- package/dist/chunk-62DQAWPF.js.map +0 -1
- package/dist/chunk-BWXLTWF7.js.map +0 -1
- package/dist/chunk-M2S27J6Z.js.map +0 -1
- package/dist/chunk-NPMW5HUS.js.map +0 -1
- package/dist/chunk-R7EETU7Z.js +0 -419
- package/dist/chunk-R7EETU7Z.js.map +0 -1
- package/dist/chunk-U44TPRST.js +0 -130
- package/dist/chunk-U44TPRST.js.map +0 -1
- package/dist/chunk-XVVNKASZ.js.map +0 -1
- package/dist/chunk-YR4LJ6AQ.js +0 -7
- package/dist/chunk-YR4LJ6AQ.js.map +0 -1
- package/dist/chunk-YV2FUEBX.js +0 -851
- package/dist/chunk-YV2FUEBX.js.map +0 -1
- package/dist/chunk-ZID3WQID.js.map +0 -1
- package/dist/extract-bundle-ONWZVV55.js.map +0 -1
- package/dist/server-GKUTC73B.js.map +0 -1
- package/dist/site-extract-repository-L6BHWVDU.js +0 -30
- /package/dist/{chunk-3HBPKR5G.js.map → chunk-FSAXLDB3.js.map} +0 -0
- /package/dist/{db-YAI5AQOI.js.map → db-N3YECFWR.js.map} +0 -0
- /package/dist/{site-extract-repository-L6BHWVDU.js.map → location-data-repository-O2VII3ON.js.map} +0 -0
- /package/dist/{worker-645BZPEK.js.map → worker-TTFXPPDK.js.map} +0 -0
|
@@ -6,12 +6,17 @@ var DB_URL = process.env.TURSO_DATABASE_URL ?? "file:./paa-api.db";
|
|
|
6
6
|
var DB_TOKEN = process.env.TURSO_AUTH_TOKEN;
|
|
7
7
|
var _db = null;
|
|
8
8
|
var _rateLimitSchemaReady = false;
|
|
9
|
-
var CORE_SCHEMA_VERSION = "2026-07-
|
|
9
|
+
var CORE_SCHEMA_VERSION = "2026-07-28.1";
|
|
10
10
|
var CORE_SCHEMA_TABLES = [
|
|
11
|
+
"admin_credit_adjustment_events",
|
|
12
|
+
"admin_credit_adjustments",
|
|
11
13
|
"billing_authorizations",
|
|
14
|
+
"billing_credits",
|
|
12
15
|
"billing_debits",
|
|
16
|
+
"billing_debit_allocations",
|
|
13
17
|
"billing_events",
|
|
14
18
|
"browser_profile_labels",
|
|
19
|
+
"connected_account_billing",
|
|
15
20
|
"concurrency_locks",
|
|
16
21
|
"credit_lots",
|
|
17
22
|
"free_credit_refreshes",
|
|
@@ -40,7 +45,10 @@ var CORE_SCHEMA_TABLES = [
|
|
|
40
45
|
"workflow_webhook_deliveries"
|
|
41
46
|
];
|
|
42
47
|
var CORE_SCHEMA_INDEXES = [
|
|
48
|
+
"admin_credit_adjustment_events_reference",
|
|
49
|
+
"admin_credit_adjustments_user_created",
|
|
43
50
|
"billing_authorizations_user_status",
|
|
51
|
+
"billing_credits_user_status",
|
|
44
52
|
"billing_debits_user_status",
|
|
45
53
|
"billing_events_authorization",
|
|
46
54
|
"billing_events_user_created_at",
|
|
@@ -65,6 +73,7 @@ var CORE_SCHEMA_INDEXES = [
|
|
|
65
73
|
"site_audit_jobs_user_id",
|
|
66
74
|
"site_audit_phase_log_job_id",
|
|
67
75
|
"site_extract_jobs_status",
|
|
76
|
+
"site_extract_jobs_idempotency",
|
|
68
77
|
"site_extract_jobs_user_id",
|
|
69
78
|
"site_extract_pages_job_id",
|
|
70
79
|
"workflow_artifacts_run",
|
|
@@ -79,6 +88,19 @@ var CORE_SCHEMA_COLUMNS = {
|
|
|
79
88
|
credit_lots: ["never_expires"],
|
|
80
89
|
harvest_attempts: ["debug_json"],
|
|
81
90
|
oauth_tokens: ["replaced_by", "rotated_at"],
|
|
91
|
+
site_extract_jobs: [
|
|
92
|
+
"attempted_urls",
|
|
93
|
+
"successful_urls",
|
|
94
|
+
"failed_urls",
|
|
95
|
+
"idempotency_key",
|
|
96
|
+
"request_fingerprint",
|
|
97
|
+
"dispatch_attempts",
|
|
98
|
+
"next_dispatch_at",
|
|
99
|
+
"dispatch_error",
|
|
100
|
+
"settlement_attempts",
|
|
101
|
+
"next_settlement_at",
|
|
102
|
+
"settlement_error"
|
|
103
|
+
],
|
|
82
104
|
users: [
|
|
83
105
|
"balance_mc",
|
|
84
106
|
"concurrency_stripe_sub_id",
|
|
@@ -112,6 +134,46 @@ async function hasCurrentCoreSchema(db) {
|
|
|
112
134
|
const result = await db.execute(`SELECT ${checks.join(" AND ")} AS ready`);
|
|
113
135
|
return Number(result.rows[0]?.ready ?? 0) === 1;
|
|
114
136
|
}
|
|
137
|
+
async function runIdempotentCoreDataBackfills(db) {
|
|
138
|
+
await db.execute(`
|
|
139
|
+
UPDATE site_extract_jobs SET
|
|
140
|
+
attempted_urls = done_urls,
|
|
141
|
+
successful_urls = (
|
|
142
|
+
SELECT COUNT(*) FROM site_extract_pages
|
|
143
|
+
WHERE job_id = site_extract_jobs.id
|
|
144
|
+
AND CASE
|
|
145
|
+
WHEN json_valid(page) = 0 THEN 0
|
|
146
|
+
WHEN json_extract(page, '$.extractionStatus') IS NOT NULL
|
|
147
|
+
THEN json_extract(page, '$.extractionStatus') = 'successful'
|
|
148
|
+
ELSE COALESCE(
|
|
149
|
+
json_extract(page, '$.status') >= 200
|
|
150
|
+
AND json_extract(page, '$.status') < 300
|
|
151
|
+
AND json_extract(page, '$.wordCount') > 0,
|
|
152
|
+
0
|
|
153
|
+
)
|
|
154
|
+
END
|
|
155
|
+
),
|
|
156
|
+
failed_urls = MAX(0, done_urls - (
|
|
157
|
+
SELECT COUNT(*) FROM site_extract_pages
|
|
158
|
+
WHERE job_id = site_extract_jobs.id
|
|
159
|
+
AND CASE
|
|
160
|
+
WHEN json_valid(page) = 0 THEN 0
|
|
161
|
+
WHEN json_extract(page, '$.extractionStatus') IS NOT NULL
|
|
162
|
+
THEN json_extract(page, '$.extractionStatus') = 'successful'
|
|
163
|
+
ELSE COALESCE(
|
|
164
|
+
json_extract(page, '$.status') >= 200
|
|
165
|
+
AND json_extract(page, '$.status') < 300
|
|
166
|
+
AND json_extract(page, '$.wordCount') > 0,
|
|
167
|
+
0
|
|
168
|
+
)
|
|
169
|
+
END
|
|
170
|
+
))
|
|
171
|
+
WHERE done_urls > 0
|
|
172
|
+
AND attempted_urls = 0
|
|
173
|
+
AND successful_urls = 0
|
|
174
|
+
AND failed_urls = 0
|
|
175
|
+
`);
|
|
176
|
+
}
|
|
115
177
|
function getDb() {
|
|
116
178
|
if (!_db) _db = createClient({ url: DB_URL, authToken: DB_TOKEN });
|
|
117
179
|
return _db;
|
|
@@ -138,8 +200,12 @@ async function migrate() {
|
|
|
138
200
|
`);
|
|
139
201
|
applied = { rows: [] };
|
|
140
202
|
}
|
|
141
|
-
if (applied.rows.length > 0 && await hasCurrentCoreSchema(db))
|
|
203
|
+
if (applied.rows.length > 0 && await hasCurrentCoreSchema(db)) {
|
|
204
|
+
await runIdempotentCoreDataBackfills(db);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
142
207
|
if (await hasCurrentCoreSchema(db)) {
|
|
208
|
+
await runIdempotentCoreDataBackfills(db);
|
|
143
209
|
await db.execute({
|
|
144
210
|
sql: "INSERT OR IGNORE INTO schema_migrations (version) VALUES (?)",
|
|
145
211
|
args: [CORE_SCHEMA_VERSION]
|
|
@@ -397,6 +463,74 @@ async function migrate() {
|
|
|
397
463
|
)
|
|
398
464
|
`);
|
|
399
465
|
await db.execute(`CREATE INDEX IF NOT EXISTS billing_debits_user_status ON billing_debits(user_id, status)`);
|
|
466
|
+
await db.execute(`
|
|
467
|
+
CREATE TABLE IF NOT EXISTS billing_debit_allocations (
|
|
468
|
+
debit_idempotency_key TEXT NOT NULL REFERENCES billing_debits(idempotency_key),
|
|
469
|
+
lot_id INTEGER NOT NULL REFERENCES credit_lots(id),
|
|
470
|
+
user_id INTEGER NOT NULL REFERENCES users(id),
|
|
471
|
+
amount_mc INTEGER NOT NULL,
|
|
472
|
+
expires_at TEXT NOT NULL,
|
|
473
|
+
PRIMARY KEY (debit_idempotency_key, lot_id)
|
|
474
|
+
)
|
|
475
|
+
`);
|
|
476
|
+
await db.execute(`
|
|
477
|
+
CREATE TABLE IF NOT EXISTS connected_account_billing (
|
|
478
|
+
user_id INTEGER PRIMARY KEY REFERENCES users(id),
|
|
479
|
+
stripe_subscription_id TEXT,
|
|
480
|
+
stripe_subscription_item_id TEXT,
|
|
481
|
+
price_id TEXT,
|
|
482
|
+
quantity INTEGER NOT NULL DEFAULT 0,
|
|
483
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
484
|
+
last_error_code TEXT,
|
|
485
|
+
checked_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
486
|
+
synced_at TEXT,
|
|
487
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
488
|
+
)
|
|
489
|
+
`);
|
|
490
|
+
await db.execute(`
|
|
491
|
+
CREATE TABLE IF NOT EXISTS billing_credits (
|
|
492
|
+
idempotency_key TEXT PRIMARY KEY,
|
|
493
|
+
user_id INTEGER NOT NULL REFERENCES users(id),
|
|
494
|
+
amount_mc INTEGER NOT NULL,
|
|
495
|
+
operation TEXT NOT NULL,
|
|
496
|
+
description TEXT,
|
|
497
|
+
status TEXT NOT NULL DEFAULT 'claimed',
|
|
498
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
499
|
+
applied_at TEXT
|
|
500
|
+
)
|
|
501
|
+
`);
|
|
502
|
+
await db.execute(`CREATE INDEX IF NOT EXISTS billing_credits_user_status ON billing_credits(user_id, status)`);
|
|
503
|
+
await db.execute(`
|
|
504
|
+
CREATE TABLE IF NOT EXISTS admin_credit_adjustments (
|
|
505
|
+
reference TEXT PRIMARY KEY,
|
|
506
|
+
user_id INTEGER NOT NULL REFERENCES users(id),
|
|
507
|
+
direction TEXT NOT NULL,
|
|
508
|
+
amount_mc INTEGER NOT NULL,
|
|
509
|
+
reason TEXT NOT NULL,
|
|
510
|
+
actor TEXT NOT NULL,
|
|
511
|
+
status TEXT NOT NULL DEFAULT 'applied',
|
|
512
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
513
|
+
updated_at TEXT
|
|
514
|
+
)
|
|
515
|
+
`);
|
|
516
|
+
await db.execute(`CREATE INDEX IF NOT EXISTS admin_credit_adjustments_user_created ON admin_credit_adjustments(user_id, created_at)`);
|
|
517
|
+
await db.execute(`
|
|
518
|
+
CREATE TABLE IF NOT EXISTS admin_credit_adjustment_events (
|
|
519
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
520
|
+
reference TEXT NOT NULL REFERENCES admin_credit_adjustments(reference),
|
|
521
|
+
seq INTEGER NOT NULL,
|
|
522
|
+
kind TEXT NOT NULL,
|
|
523
|
+
from_amount_mc INTEGER,
|
|
524
|
+
to_amount_mc INTEGER,
|
|
525
|
+
from_reason TEXT,
|
|
526
|
+
to_reason TEXT,
|
|
527
|
+
moved_mc INTEGER NOT NULL DEFAULT 0,
|
|
528
|
+
actor TEXT NOT NULL,
|
|
529
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
530
|
+
UNIQUE (reference, seq)
|
|
531
|
+
)
|
|
532
|
+
`);
|
|
533
|
+
await db.execute(`CREATE INDEX IF NOT EXISTS admin_credit_adjustment_events_reference ON admin_credit_adjustment_events(reference)`);
|
|
400
534
|
await db.execute(`
|
|
401
535
|
CREATE TABLE IF NOT EXISTS site_audit_jobs (
|
|
402
536
|
id TEXT PRIMARY KEY,
|
|
@@ -432,6 +566,17 @@ async function migrate() {
|
|
|
432
566
|
options TEXT,
|
|
433
567
|
total_urls INTEGER DEFAULT 0,
|
|
434
568
|
done_urls INTEGER DEFAULT 0,
|
|
569
|
+
attempted_urls INTEGER DEFAULT 0,
|
|
570
|
+
successful_urls INTEGER DEFAULT 0,
|
|
571
|
+
failed_urls INTEGER DEFAULT 0,
|
|
572
|
+
idempotency_key TEXT,
|
|
573
|
+
request_fingerprint TEXT,
|
|
574
|
+
dispatch_attempts INTEGER DEFAULT 0,
|
|
575
|
+
next_dispatch_at TEXT,
|
|
576
|
+
dispatch_error TEXT,
|
|
577
|
+
settlement_attempts INTEGER DEFAULT 0,
|
|
578
|
+
next_settlement_at TEXT,
|
|
579
|
+
settlement_error TEXT,
|
|
435
580
|
artifacts TEXT,
|
|
436
581
|
error TEXT,
|
|
437
582
|
billed_mc INTEGER,
|
|
@@ -450,6 +595,51 @@ async function migrate() {
|
|
|
450
595
|
await db.execute(`CREATE INDEX IF NOT EXISTS site_extract_jobs_user_id ON site_extract_jobs(user_id)`);
|
|
451
596
|
await db.execute(`CREATE INDEX IF NOT EXISTS site_extract_jobs_status ON site_extract_jobs(status)`);
|
|
452
597
|
await db.execute(`CREATE INDEX IF NOT EXISTS site_extract_pages_job_id ON site_extract_pages(job_id)`);
|
|
598
|
+
try {
|
|
599
|
+
await db.execute(`ALTER TABLE site_extract_jobs ADD COLUMN attempted_urls INTEGER DEFAULT 0`);
|
|
600
|
+
} catch {
|
|
601
|
+
}
|
|
602
|
+
try {
|
|
603
|
+
await db.execute(`ALTER TABLE site_extract_jobs ADD COLUMN successful_urls INTEGER DEFAULT 0`);
|
|
604
|
+
} catch {
|
|
605
|
+
}
|
|
606
|
+
try {
|
|
607
|
+
await db.execute(`ALTER TABLE site_extract_jobs ADD COLUMN failed_urls INTEGER DEFAULT 0`);
|
|
608
|
+
} catch {
|
|
609
|
+
}
|
|
610
|
+
try {
|
|
611
|
+
await db.execute(`ALTER TABLE site_extract_jobs ADD COLUMN idempotency_key TEXT`);
|
|
612
|
+
} catch {
|
|
613
|
+
}
|
|
614
|
+
try {
|
|
615
|
+
await db.execute(`ALTER TABLE site_extract_jobs ADD COLUMN request_fingerprint TEXT`);
|
|
616
|
+
} catch {
|
|
617
|
+
}
|
|
618
|
+
try {
|
|
619
|
+
await db.execute(`ALTER TABLE site_extract_jobs ADD COLUMN dispatch_attempts INTEGER DEFAULT 0`);
|
|
620
|
+
} catch {
|
|
621
|
+
}
|
|
622
|
+
try {
|
|
623
|
+
await db.execute(`ALTER TABLE site_extract_jobs ADD COLUMN next_dispatch_at TEXT`);
|
|
624
|
+
} catch {
|
|
625
|
+
}
|
|
626
|
+
try {
|
|
627
|
+
await db.execute(`ALTER TABLE site_extract_jobs ADD COLUMN dispatch_error TEXT`);
|
|
628
|
+
} catch {
|
|
629
|
+
}
|
|
630
|
+
try {
|
|
631
|
+
await db.execute(`ALTER TABLE site_extract_jobs ADD COLUMN settlement_attempts INTEGER DEFAULT 0`);
|
|
632
|
+
} catch {
|
|
633
|
+
}
|
|
634
|
+
try {
|
|
635
|
+
await db.execute(`ALTER TABLE site_extract_jobs ADD COLUMN next_settlement_at TEXT`);
|
|
636
|
+
} catch {
|
|
637
|
+
}
|
|
638
|
+
try {
|
|
639
|
+
await db.execute(`ALTER TABLE site_extract_jobs ADD COLUMN settlement_error TEXT`);
|
|
640
|
+
} catch {
|
|
641
|
+
}
|
|
642
|
+
await db.execute(`CREATE UNIQUE INDEX IF NOT EXISTS site_extract_jobs_idempotency ON site_extract_jobs(user_id, idempotency_key) WHERE idempotency_key IS NOT NULL`);
|
|
453
643
|
await db.execute(`
|
|
454
644
|
CREATE TABLE IF NOT EXISTS password_reset_tokens (
|
|
455
645
|
token TEXT PRIMARY KEY,
|
|
@@ -670,6 +860,7 @@ async function migrate() {
|
|
|
670
860
|
if (!await hasCurrentCoreSchema(db)) {
|
|
671
861
|
throw new Error("core schema migration finished without all required tables, indexes, and columns");
|
|
672
862
|
}
|
|
863
|
+
await runIdempotentCoreDataBackfills(db);
|
|
673
864
|
await db.execute({
|
|
674
865
|
sql: "INSERT OR IGNORE INTO schema_migrations (version) VALUES (?)",
|
|
675
866
|
args: [CORE_SCHEMA_VERSION]
|
|
@@ -1647,6 +1838,75 @@ async function setSubscriptionTier(userId, tier, concurrency, subId) {
|
|
|
1647
1838
|
args: [tier, Math.max(0, Math.round(concurrency)), subId, userId]
|
|
1648
1839
|
});
|
|
1649
1840
|
}
|
|
1841
|
+
function rowToConnectedAccountBillingState(row) {
|
|
1842
|
+
return {
|
|
1843
|
+
user_id: Number(row.user_id),
|
|
1844
|
+
stripe_subscription_id: row.stripe_subscription_id != null ? String(row.stripe_subscription_id) : null,
|
|
1845
|
+
stripe_subscription_item_id: row.stripe_subscription_item_id != null ? String(row.stripe_subscription_item_id) : null,
|
|
1846
|
+
price_id: row.price_id != null ? String(row.price_id) : null,
|
|
1847
|
+
quantity: Math.max(0, Number(row.quantity ?? 0)),
|
|
1848
|
+
status: ["pending", "synced", "blocked", "error"].includes(String(row.status)) ? String(row.status) : "error",
|
|
1849
|
+
last_error_code: row.last_error_code != null ? String(row.last_error_code) : null,
|
|
1850
|
+
checked_at: String(row.checked_at),
|
|
1851
|
+
synced_at: row.synced_at != null ? String(row.synced_at) : null,
|
|
1852
|
+
updated_at: String(row.updated_at)
|
|
1853
|
+
};
|
|
1854
|
+
}
|
|
1855
|
+
async function getConnectedAccountBillingState(userId) {
|
|
1856
|
+
const res = await getDb().execute({
|
|
1857
|
+
sql: "SELECT * FROM connected_account_billing WHERE user_id = ? LIMIT 1",
|
|
1858
|
+
args: [userId]
|
|
1859
|
+
});
|
|
1860
|
+
const row = res.rows[0];
|
|
1861
|
+
return row ? rowToConnectedAccountBillingState(row) : null;
|
|
1862
|
+
}
|
|
1863
|
+
async function setConnectedAccountBillingState(input) {
|
|
1864
|
+
const quantity = Math.max(0, Math.round(input.quantity));
|
|
1865
|
+
await getDb().execute({
|
|
1866
|
+
sql: `INSERT INTO connected_account_billing
|
|
1867
|
+
(user_id, stripe_subscription_id, stripe_subscription_item_id, price_id, quantity, status, last_error_code, checked_at, synced_at, updated_at)
|
|
1868
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now'), CASE WHEN ? THEN datetime('now') ELSE NULL END, datetime('now'))
|
|
1869
|
+
ON CONFLICT(user_id) DO UPDATE SET
|
|
1870
|
+
stripe_subscription_id = excluded.stripe_subscription_id,
|
|
1871
|
+
stripe_subscription_item_id = excluded.stripe_subscription_item_id,
|
|
1872
|
+
price_id = excluded.price_id,
|
|
1873
|
+
quantity = excluded.quantity,
|
|
1874
|
+
status = excluded.status,
|
|
1875
|
+
last_error_code = excluded.last_error_code,
|
|
1876
|
+
checked_at = datetime('now'),
|
|
1877
|
+
synced_at = CASE WHEN ? THEN datetime('now') ELSE connected_account_billing.synced_at END,
|
|
1878
|
+
updated_at = datetime('now')`,
|
|
1879
|
+
args: [
|
|
1880
|
+
input.userId,
|
|
1881
|
+
input.stripeSubscriptionId,
|
|
1882
|
+
input.stripeSubscriptionItemId,
|
|
1883
|
+
input.priceId,
|
|
1884
|
+
quantity,
|
|
1885
|
+
input.status,
|
|
1886
|
+
input.lastErrorCode ?? null,
|
|
1887
|
+
input.synced === true ? 1 : 0,
|
|
1888
|
+
input.synced === true ? 1 : 0
|
|
1889
|
+
]
|
|
1890
|
+
});
|
|
1891
|
+
const state = await getConnectedAccountBillingState(input.userId);
|
|
1892
|
+
if (!state) throw new Error("connected account billing state was not persisted");
|
|
1893
|
+
return state;
|
|
1894
|
+
}
|
|
1895
|
+
async function listUsersDueForConnectedAccountBillingReconciliation(staleMinutes = 60, limit = 20) {
|
|
1896
|
+
const modifier = `-${Math.max(1, Math.round(staleMinutes))} minutes`;
|
|
1897
|
+
const res = await getDb().execute({
|
|
1898
|
+
sql: `SELECT u.*
|
|
1899
|
+
FROM users u
|
|
1900
|
+
LEFT JOIN connected_account_billing b ON b.user_id = u.id
|
|
1901
|
+
WHERE u.active = 1
|
|
1902
|
+
AND u.subscription_id IS NOT NULL
|
|
1903
|
+
AND (b.checked_at IS NULL OR b.checked_at <= datetime('now', ?))
|
|
1904
|
+
ORDER BY COALESCE(b.checked_at, u.created_at) ASC
|
|
1905
|
+
LIMIT ?`,
|
|
1906
|
+
args: [modifier, Math.max(1, Math.min(100, Math.round(limit)))]
|
|
1907
|
+
});
|
|
1908
|
+
return res.rows.map((row) => rowToUser(row));
|
|
1909
|
+
}
|
|
1650
1910
|
async function setMemoryPlan(userId, plan, subscriptionId) {
|
|
1651
1911
|
await getDb().execute({
|
|
1652
1912
|
sql: "UPDATE users SET memory_plan = ?, memory_subscription_id = ? WHERE id = ?",
|
|
@@ -1784,6 +2044,84 @@ async function creditMc(userId, mc, operation, description, stripePaymentIntent,
|
|
|
1784
2044
|
const res = await db.execute({ sql: "SELECT balance_mc FROM users WHERE id = ?", args: [userId] });
|
|
1785
2045
|
return Number(res.rows[0]?.balance_mc ?? 0);
|
|
1786
2046
|
}
|
|
2047
|
+
async function creditMcIdempotent(userId, mc, operation, description, idempotencyKey, options) {
|
|
2048
|
+
if (!Number.isSafeInteger(mc) || mc <= 0) throw new Error("idempotent credit amount must be a positive integer");
|
|
2049
|
+
if (!idempotencyKey || idempotencyKey.length > 500) throw new Error("idempotent credit key is required and must be bounded");
|
|
2050
|
+
const db = getDb();
|
|
2051
|
+
await ensureMigrated(userId);
|
|
2052
|
+
let neverExpires = options?.neverExpires;
|
|
2053
|
+
if (neverExpires === void 0) {
|
|
2054
|
+
const tier = await db.execute({ sql: "SELECT subscription_tier FROM users WHERE id = ? LIMIT 1", args: [userId] });
|
|
2055
|
+
neverExpires = String(tier.rows[0]?.subscription_tier ?? "").startsWith("sonic");
|
|
2056
|
+
}
|
|
2057
|
+
const expiry = neverExpires ? "9999-12-31 23:59:59" : null;
|
|
2058
|
+
const readBalance = async () => {
|
|
2059
|
+
const res = await db.execute({ sql: "SELECT balance_mc FROM users WHERE id = ?", args: [userId] });
|
|
2060
|
+
return Number(res.rows[0]?.balance_mc ?? 0);
|
|
2061
|
+
};
|
|
2062
|
+
let results;
|
|
2063
|
+
try {
|
|
2064
|
+
results = await db.batch([
|
|
2065
|
+
{
|
|
2066
|
+
sql: "INSERT OR IGNORE INTO billing_credits (idempotency_key, user_id, amount_mc, operation, description, status) VALUES (?, ?, ?, ?, ?, 'claimed')",
|
|
2067
|
+
args: [idempotencyKey, userId, mc, operation, description]
|
|
2068
|
+
},
|
|
2069
|
+
{
|
|
2070
|
+
sql: "UPDATE billing_credits SET status = 'applying' WHERE idempotency_key = ? AND user_id = ? AND amount_mc = ? AND operation = ? AND description = ? AND status = 'claimed'",
|
|
2071
|
+
args: [idempotencyKey, userId, mc, operation, description]
|
|
2072
|
+
},
|
|
2073
|
+
{
|
|
2074
|
+
sql: `INSERT INTO credit_lots (user_id, amount_mc, remaining_mc, source, stripe_pi, expires_at, never_expires)
|
|
2075
|
+
SELECT user_id, amount_mc, amount_mc, operation, NULL, COALESCE(?, datetime('now', ?)), ?
|
|
2076
|
+
FROM billing_credits
|
|
2077
|
+
WHERE idempotency_key = ? AND status = 'applying'`,
|
|
2078
|
+
args: [expiry, CREDIT_LOT_TTL, neverExpires ? 1 : 0, idempotencyKey]
|
|
2079
|
+
},
|
|
2080
|
+
{
|
|
2081
|
+
sql: `UPDATE users
|
|
2082
|
+
SET balance_mc = balance_mc + COALESCE((
|
|
2083
|
+
SELECT amount_mc FROM billing_credits WHERE idempotency_key = ? AND status = 'applying'
|
|
2084
|
+
), 0)
|
|
2085
|
+
WHERE id = ?`,
|
|
2086
|
+
args: [idempotencyKey, userId]
|
|
2087
|
+
},
|
|
2088
|
+
{
|
|
2089
|
+
sql: "INSERT INTO ledger (user_id, amount_mc, operation, description) SELECT user_id, amount_mc, operation, description FROM billing_credits WHERE idempotency_key = ? AND status = 'applying'",
|
|
2090
|
+
args: [idempotencyKey]
|
|
2091
|
+
},
|
|
2092
|
+
{
|
|
2093
|
+
sql: "UPDATE billing_credits SET status = 'applied', applied_at = datetime('now') WHERE idempotency_key = ? AND status = 'applying'",
|
|
2094
|
+
args: [idempotencyKey]
|
|
2095
|
+
}
|
|
2096
|
+
], "write");
|
|
2097
|
+
} catch (error) {
|
|
2098
|
+
try {
|
|
2099
|
+
const recovered = await db.execute({
|
|
2100
|
+
sql: "SELECT user_id, amount_mc, operation, description FROM billing_credits WHERE idempotency_key = ? AND status = 'applied' LIMIT 1",
|
|
2101
|
+
args: [idempotencyKey]
|
|
2102
|
+
});
|
|
2103
|
+
const row2 = recovered.rows[0];
|
|
2104
|
+
if (row2 && Number(row2.user_id) === userId && Number(row2.amount_mc) === mc && String(row2.operation) === operation && String(row2.description) === description) {
|
|
2105
|
+
return { ok: true, balance_mc: await readBalance(), duplicate: true };
|
|
2106
|
+
}
|
|
2107
|
+
} catch {
|
|
2108
|
+
}
|
|
2109
|
+
throw error;
|
|
2110
|
+
}
|
|
2111
|
+
const claim = await db.execute({
|
|
2112
|
+
sql: "SELECT user_id, amount_mc, operation, description, status FROM billing_credits WHERE idempotency_key = ? LIMIT 1",
|
|
2113
|
+
args: [idempotencyKey]
|
|
2114
|
+
});
|
|
2115
|
+
const row = claim.rows[0];
|
|
2116
|
+
if (!row || Number(row.user_id) !== userId || Number(row.amount_mc) !== mc || String(row.operation) !== operation || String(row.description) !== description) {
|
|
2117
|
+
throw new Error("idempotent credit key conflicts with another credit");
|
|
2118
|
+
}
|
|
2119
|
+
return {
|
|
2120
|
+
ok: String(row.status) === "applied",
|
|
2121
|
+
balance_mc: await readBalance(),
|
|
2122
|
+
duplicate: results[1]?.rowsAffected === 0 && String(row.status) === "applied"
|
|
2123
|
+
};
|
|
2124
|
+
}
|
|
1787
2125
|
async function ledgerExistsForOperation(userId, operation) {
|
|
1788
2126
|
const res = await getDb().execute({
|
|
1789
2127
|
sql: "SELECT 1 FROM ledger WHERE user_id = ? AND operation = ? LIMIT 1",
|
|
@@ -1826,6 +2164,8 @@ async function debitMcIdempotent(userId, mc, operation, description, idempotency
|
|
|
1826
2164
|
try {
|
|
1827
2165
|
results = await db.batch([
|
|
1828
2166
|
{
|
|
2167
|
+
// An unfunded claim may be retried with a corrected estimate under the
|
|
2168
|
+
// same logical operation fingerprint. Applied claims remain immutable.
|
|
1829
2169
|
sql: "UPDATE billing_debits SET amount_mc = ? WHERE idempotency_key = ? AND user_id = ? AND operation = ? AND description = ? AND status = 'claimed'",
|
|
1830
2170
|
args: [mc, idempotencyKey, userId, operation, description]
|
|
1831
2171
|
},
|
|
@@ -1834,17 +2174,29 @@ async function debitMcIdempotent(userId, mc, operation, description, idempotency
|
|
|
1834
2174
|
args: [idempotencyKey, userId, mc, operation, description]
|
|
1835
2175
|
},
|
|
1836
2176
|
{
|
|
1837
|
-
sql:
|
|
1838
|
-
|
|
2177
|
+
sql: `UPDATE users
|
|
2178
|
+
SET balance_mc = balance_mc - ?
|
|
2179
|
+
WHERE id = ?
|
|
2180
|
+
AND balance_mc >= ?
|
|
2181
|
+
AND (SELECT COALESCE(SUM(remaining_mc), 0)
|
|
2182
|
+
FROM credit_lots
|
|
2183
|
+
WHERE user_id = ?
|
|
2184
|
+
AND remaining_mc > 0
|
|
2185
|
+
AND (never_expires = 1 OR expires_at > datetime('now'))) >= ?
|
|
2186
|
+
AND EXISTS (SELECT 1 FROM billing_debits WHERE idempotency_key = ? AND user_id = ? AND amount_mc = ? AND operation = ? AND description = ? AND status = 'claimed')`,
|
|
2187
|
+
args: [mc, userId, mc, userId, mc, idempotencyKey, userId, mc, operation, description]
|
|
1839
2188
|
},
|
|
1840
2189
|
{
|
|
1841
2190
|
sql: "UPDATE billing_debits SET status = 'applying' WHERE idempotency_key = ? AND user_id = ? AND amount_mc = ? AND operation = ? AND description = ? AND status = 'claimed' AND changes() = 1",
|
|
1842
2191
|
args: [idempotencyKey, userId, mc, operation, description]
|
|
1843
2192
|
},
|
|
1844
2193
|
{
|
|
1845
|
-
sql: `
|
|
2194
|
+
sql: `INSERT OR IGNORE INTO billing_debit_allocations
|
|
2195
|
+
(debit_idempotency_key, lot_id, user_id, amount_mc, expires_at)
|
|
2196
|
+
WITH ordered AS MATERIALIZED (
|
|
1846
2197
|
SELECT id,
|
|
1847
2198
|
remaining_mc,
|
|
2199
|
+
expires_at,
|
|
1848
2200
|
COALESCE(SUM(remaining_mc) OVER (
|
|
1849
2201
|
ORDER BY never_expires ASC, expires_at ASC, granted_at ASC, id ASC
|
|
1850
2202
|
ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING
|
|
@@ -1852,14 +2204,26 @@ async function debitMcIdempotent(userId, mc, operation, description, idempotency
|
|
|
1852
2204
|
FROM credit_lots
|
|
1853
2205
|
WHERE user_id = ? AND remaining_mc > 0 AND (never_expires = 1 OR expires_at > datetime('now'))
|
|
1854
2206
|
), allocation AS (
|
|
1855
|
-
SELECT id, MIN(remaining_mc, MAX(0, ? - prior_mc)) AS take_mc
|
|
2207
|
+
SELECT id, expires_at, MIN(remaining_mc, MAX(0, ? - prior_mc)) AS take_mc
|
|
1856
2208
|
FROM ordered
|
|
1857
2209
|
)
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
WHERE
|
|
2210
|
+
SELECT ?, id, ?, take_mc, expires_at
|
|
2211
|
+
FROM allocation
|
|
2212
|
+
WHERE take_mc > 0
|
|
1861
2213
|
AND EXISTS (SELECT 1 FROM billing_debits WHERE idempotency_key = ? AND status = 'applying')`,
|
|
1862
|
-
args: [userId, mc, idempotencyKey]
|
|
2214
|
+
args: [userId, mc, idempotencyKey, userId, idempotencyKey]
|
|
2215
|
+
},
|
|
2216
|
+
{
|
|
2217
|
+
sql: `UPDATE credit_lots
|
|
2218
|
+
SET remaining_mc = remaining_mc - COALESCE((
|
|
2219
|
+
SELECT amount_mc FROM billing_debit_allocations
|
|
2220
|
+
WHERE debit_idempotency_key = ? AND lot_id = credit_lots.id
|
|
2221
|
+
), 0)
|
|
2222
|
+
WHERE id IN (
|
|
2223
|
+
SELECT lot_id FROM billing_debit_allocations WHERE debit_idempotency_key = ?
|
|
2224
|
+
)
|
|
2225
|
+
AND EXISTS (SELECT 1 FROM billing_debits WHERE idempotency_key = ? AND status = 'applying')`,
|
|
2226
|
+
args: [idempotencyKey, idempotencyKey, idempotencyKey]
|
|
1863
2227
|
},
|
|
1864
2228
|
{
|
|
1865
2229
|
sql: "INSERT INTO ledger (user_id, amount_mc, operation, description) SELECT user_id, -amount_mc, operation, description FROM billing_debits WHERE idempotency_key = ? AND status = 'applying'",
|
|
@@ -1923,6 +2287,7 @@ async function settleDebitMcIdempotent(userId, debitIdempotencyKey, finalAmountM
|
|
|
1923
2287
|
e.amount_mc AS final_amount_mc,
|
|
1924
2288
|
e.billing_class,
|
|
1925
2289
|
e.source_surface,
|
|
2290
|
+
e.settled_at,
|
|
1926
2291
|
d.amount_mc AS held_amount_mc
|
|
1927
2292
|
FROM billing_events e
|
|
1928
2293
|
JOIN billing_debits d ON d.idempotency_key = ?
|
|
@@ -1943,11 +2308,35 @@ async function settleDebitMcIdempotent(userId, debitIdempotencyKey, finalAmountM
|
|
|
1943
2308
|
if (!Number.isSafeInteger(heldAmountMc) || !Number.isSafeInteger(durableFinalAmountMc) || durableFinalAmountMc < 0 || durableFinalAmountMc > heldAmountMc) {
|
|
1944
2309
|
throw new Error("idempotent debit settlement receipt is invalid");
|
|
1945
2310
|
}
|
|
2311
|
+
const nominalRefundMc = heldAmountMc - durableFinalAmountMc;
|
|
2312
|
+
const allocationRefund = await db.execute({
|
|
2313
|
+
sql: `WITH ordered AS (
|
|
2314
|
+
SELECT a.amount_mc,
|
|
2315
|
+
a.expires_at,
|
|
2316
|
+
COALESCE(SUM(a.amount_mc) OVER (
|
|
2317
|
+
ORDER BY a.expires_at ASC, a.lot_id ASC
|
|
2318
|
+
ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING
|
|
2319
|
+
), 0) AS prior_mc
|
|
2320
|
+
FROM billing_debit_allocations a
|
|
2321
|
+
WHERE a.debit_idempotency_key = ?
|
|
2322
|
+
), refunds AS (
|
|
2323
|
+
SELECT expires_at,
|
|
2324
|
+
amount_mc - MIN(amount_mc, MAX(0, ? - prior_mc)) AS refund_mc
|
|
2325
|
+
FROM ordered
|
|
2326
|
+
)
|
|
2327
|
+
SELECT (SELECT COUNT(*) FROM ordered) AS allocation_count,
|
|
2328
|
+
COALESCE(SUM(CASE WHEN expires_at > ? THEN refund_mc ELSE 0 END), 0) AS credited_refund_mc
|
|
2329
|
+
FROM refunds`,
|
|
2330
|
+
args: [debitIdempotencyKey, durableFinalAmountMc, String(row.settled_at)]
|
|
2331
|
+
});
|
|
2332
|
+
const allocationCount = Number(allocationRefund.rows[0]?.allocation_count ?? 0);
|
|
2333
|
+
const creditedRefundMc = allocationCount > 0 ? Number(allocationRefund.rows[0]?.credited_refund_mc ?? 0) : 0;
|
|
1946
2334
|
const balance = await db.execute({ sql: "SELECT balance_mc FROM users WHERE id = ?", args: [userId] });
|
|
1947
2335
|
return {
|
|
1948
2336
|
balance_mc: Number(balance.rows[0]?.balance_mc ?? 0),
|
|
1949
2337
|
final_amount_mc: durableFinalAmountMc,
|
|
1950
|
-
refund_mc:
|
|
2338
|
+
refund_mc: creditedRefundMc,
|
|
2339
|
+
expired_refund_mc: nominalRefundMc - creditedRefundMc,
|
|
1951
2340
|
duplicate: true
|
|
1952
2341
|
};
|
|
1953
2342
|
};
|
|
@@ -1961,13 +2350,34 @@ async function settleDebitMcIdempotent(userId, debitIdempotencyKey, finalAmountM
|
|
|
1961
2350
|
args: [eventId, userId, settlementKey, billingClass, sourceSurface, finalAmountMc, metadata]
|
|
1962
2351
|
},
|
|
1963
2352
|
{
|
|
1964
|
-
sql: `
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
2353
|
+
sql: `WITH ordered AS (
|
|
2354
|
+
SELECT a.lot_id,
|
|
2355
|
+
a.amount_mc,
|
|
2356
|
+
a.expires_at,
|
|
2357
|
+
COALESCE(SUM(a.amount_mc) OVER (
|
|
2358
|
+
ORDER BY a.expires_at ASC, a.lot_id ASC
|
|
2359
|
+
ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING
|
|
2360
|
+
), 0) AS prior_mc
|
|
2361
|
+
FROM billing_debit_allocations a
|
|
2362
|
+
WHERE a.debit_idempotency_key = ?
|
|
2363
|
+
), refunds AS (
|
|
2364
|
+
SELECT lot_id,
|
|
2365
|
+
expires_at,
|
|
2366
|
+
amount_mc - MIN(amount_mc, MAX(0, ? - prior_mc)) AS refund_mc
|
|
2367
|
+
FROM ordered
|
|
1970
2368
|
)
|
|
2369
|
+
UPDATE users
|
|
2370
|
+
SET balance_mc = balance_mc + CASE
|
|
2371
|
+
WHEN EXISTS (SELECT 1 FROM ordered) THEN COALESCE(
|
|
2372
|
+
(SELECT SUM(refund_mc) FROM refunds WHERE expires_at > datetime('now')),
|
|
2373
|
+
0
|
|
2374
|
+
)
|
|
2375
|
+
-- Holds created before allocation provenance cannot be safely
|
|
2376
|
+
-- tied back to a source lot. Treat their unused portion as
|
|
2377
|
+
-- expired instead of minting spendable credits with a guessed
|
|
2378
|
+
-- expiry.
|
|
2379
|
+
ELSE 0
|
|
2380
|
+
END
|
|
1971
2381
|
WHERE id = ?
|
|
1972
2382
|
AND EXISTS (
|
|
1973
2383
|
SELECT 1
|
|
@@ -1984,7 +2394,7 @@ async function settleDebitMcIdempotent(userId, debitIdempotencyKey, finalAmountM
|
|
|
1984
2394
|
)`,
|
|
1985
2395
|
args: [
|
|
1986
2396
|
debitIdempotencyKey,
|
|
1987
|
-
|
|
2397
|
+
finalAmountMc,
|
|
1988
2398
|
userId,
|
|
1989
2399
|
debitIdempotencyKey,
|
|
1990
2400
|
settlementKey,
|
|
@@ -2005,34 +2415,74 @@ async function settleDebitMcIdempotent(userId, debitIdempotencyKey, finalAmountM
|
|
|
2005
2415
|
args: [settlementKey, userId, billingClass, sourceSurface]
|
|
2006
2416
|
},
|
|
2007
2417
|
{
|
|
2008
|
-
sql: `
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2418
|
+
sql: `WITH ordered AS (
|
|
2419
|
+
SELECT a.lot_id,
|
|
2420
|
+
a.amount_mc,
|
|
2421
|
+
a.expires_at,
|
|
2422
|
+
COALESCE(SUM(a.amount_mc) OVER (
|
|
2423
|
+
ORDER BY a.expires_at ASC, a.lot_id ASC
|
|
2424
|
+
ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING
|
|
2425
|
+
), 0) AS prior_mc
|
|
2426
|
+
FROM billing_debit_allocations a
|
|
2427
|
+
WHERE a.debit_idempotency_key = ?
|
|
2428
|
+
), refunds AS (
|
|
2429
|
+
SELECT lot_id,
|
|
2430
|
+
expires_at,
|
|
2431
|
+
amount_mc - MIN(amount_mc, MAX(0, ? - prior_mc)) AS refund_mc
|
|
2432
|
+
FROM ordered
|
|
2433
|
+
)
|
|
2434
|
+
UPDATE credit_lots
|
|
2435
|
+
SET remaining_mc = remaining_mc + COALESCE((SELECT refund_mc FROM refunds WHERE refunds.lot_id = credit_lots.id), 0)
|
|
2436
|
+
WHERE id IN (
|
|
2437
|
+
SELECT lot_id FROM refunds
|
|
2438
|
+
WHERE refund_mc > 0 AND expires_at > datetime('now')
|
|
2439
|
+
)
|
|
2440
|
+
AND EXISTS (SELECT 1 FROM billing_events WHERE idempotency_key = ? AND status = 'applying')`,
|
|
2441
|
+
args: [debitIdempotencyKey, finalAmountMc, settlementKey]
|
|
2023
2442
|
},
|
|
2024
2443
|
{
|
|
2025
|
-
sql: `
|
|
2026
|
-
|
|
2027
|
-
|
|
2444
|
+
sql: `WITH ordered AS (
|
|
2445
|
+
SELECT a.amount_mc,
|
|
2446
|
+
a.expires_at,
|
|
2447
|
+
COALESCE(SUM(a.amount_mc) OVER (
|
|
2448
|
+
ORDER BY a.expires_at ASC, a.lot_id ASC
|
|
2449
|
+
ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING
|
|
2450
|
+
), 0) AS prior_mc
|
|
2451
|
+
FROM billing_debit_allocations a
|
|
2452
|
+
WHERE a.debit_idempotency_key = ?
|
|
2453
|
+
), refunds AS (
|
|
2454
|
+
SELECT expires_at,
|
|
2455
|
+
amount_mc - MIN(amount_mc, MAX(0, ? - prior_mc)) AS refund_mc
|
|
2456
|
+
FROM ordered
|
|
2457
|
+
), credited AS (
|
|
2458
|
+
SELECT e.user_id,
|
|
2459
|
+
CASE
|
|
2460
|
+
WHEN EXISTS (SELECT 1 FROM ordered) THEN COALESCE(
|
|
2461
|
+
(SELECT SUM(refund_mc) FROM refunds WHERE expires_at > datetime('now')),
|
|
2462
|
+
0
|
|
2463
|
+
)
|
|
2464
|
+
ELSE 0
|
|
2465
|
+
END AS refund_mc
|
|
2466
|
+
FROM billing_events e
|
|
2467
|
+
JOIN billing_debits d ON d.idempotency_key = ?
|
|
2468
|
+
WHERE e.idempotency_key = ?
|
|
2469
|
+
AND e.status = 'applying'
|
|
2470
|
+
)
|
|
2471
|
+
INSERT INTO ledger (user_id, amount_mc, operation, description)
|
|
2472
|
+
SELECT user_id,
|
|
2473
|
+
refund_mc,
|
|
2028
2474
|
?,
|
|
2029
2475
|
?
|
|
2030
|
-
FROM
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2476
|
+
FROM credited
|
|
2477
|
+
WHERE refund_mc > 0`,
|
|
2478
|
+
args: [
|
|
2479
|
+
debitIdempotencyKey,
|
|
2480
|
+
finalAmountMc,
|
|
2481
|
+
debitIdempotencyKey,
|
|
2482
|
+
settlementKey,
|
|
2483
|
+
refundOperation,
|
|
2484
|
+
description
|
|
2485
|
+
]
|
|
2036
2486
|
},
|
|
2037
2487
|
{
|
|
2038
2488
|
sql: `UPDATE billing_events
|
|
@@ -2228,6 +2678,7 @@ async function listActiveUserIds() {
|
|
|
2228
2678
|
}
|
|
2229
2679
|
|
|
2230
2680
|
export {
|
|
2681
|
+
CORE_SCHEMA_VERSION,
|
|
2231
2682
|
getDb,
|
|
2232
2683
|
setDbForTesting,
|
|
2233
2684
|
migrate,
|
|
@@ -2311,6 +2762,9 @@ export {
|
|
|
2311
2762
|
setExtraConcurrencySlots,
|
|
2312
2763
|
setConcurrencySubId,
|
|
2313
2764
|
setSubscriptionTier,
|
|
2765
|
+
getConnectedAccountBillingState,
|
|
2766
|
+
setConnectedAccountBillingState,
|
|
2767
|
+
listUsersDueForConnectedAccountBillingReconciliation,
|
|
2314
2768
|
setMemoryPlan,
|
|
2315
2769
|
getMemoryPlan,
|
|
2316
2770
|
getMemoryProvisioned,
|
|
@@ -2320,6 +2774,7 @@ export {
|
|
|
2320
2774
|
expireOldLots,
|
|
2321
2775
|
migrateExistingBalancesToLots,
|
|
2322
2776
|
creditMc,
|
|
2777
|
+
creditMcIdempotent,
|
|
2323
2778
|
ledgerExistsForOperation,
|
|
2324
2779
|
claimMonthlyFreeRefresh,
|
|
2325
2780
|
debitMc,
|
|
@@ -2338,4 +2793,4 @@ export {
|
|
|
2338
2793
|
insertInboxMessage,
|
|
2339
2794
|
listActiveUserIds
|
|
2340
2795
|
};
|
|
2341
|
-
//# sourceMappingURL=chunk-
|
|
2796
|
+
//# sourceMappingURL=chunk-CSCD2HNS.js.map
|