← back to Wallco Ai
marketplace: end-to-end integration test for admin payout export
8e3051466c468b19da98e9409a79f087ecaa458e · 2026-05-13 17:30:13 -0700 · SteveStudio2
Closes the validation-loop gap on commit bd49413 — the unit tests covered
pure helpers (csvEscape/aggregate/etc) and smoke covered auth-gating, but
the actual SQL JOIN + CSV write + ledger flip with payout_batch_id was
never exercised against a real DB.
This test seeds a user + designer + 3 'payable' ledger rows ($55 total
across 2 entry types), runs payouts.exportPayoutBatch(), then asserts:
- batch row created with status='exported', notes, csv_export_path
- CSV file exists on disk with the spec header line
- row for the test designer has correct designer_id, email, payout_method,
total_amount=55, and memo mentions both entry types
- all 3 seeded ledger rows are status='paid' with paid_at set and
payout_batch_id linked to the new batch
- caught + fixed bigint-as-string pg gotcha (designerId comes back as
string '21' even though column is BIGINT; assertion now Number()-coerces
both sides)
Skips with exit 0 if PG is unreachable. Cleans up all seeded rows + CSV
file in a finally{} block so re-runs don't leak state.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Files touched
A tests/marketplace/payouts-integration.test.js
Diff
commit 8e3051466c468b19da98e9409a79f087ecaa458e
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date: Wed May 13 17:30:13 2026 -0700
marketplace: end-to-end integration test for admin payout export
Closes the validation-loop gap on commit bd49413 — the unit tests covered
pure helpers (csvEscape/aggregate/etc) and smoke covered auth-gating, but
the actual SQL JOIN + CSV write + ledger flip with payout_batch_id was
never exercised against a real DB.
This test seeds a user + designer + 3 'payable' ledger rows ($55 total
across 2 entry types), runs payouts.exportPayoutBatch(), then asserts:
- batch row created with status='exported', notes, csv_export_path
- CSV file exists on disk with the spec header line
- row for the test designer has correct designer_id, email, payout_method,
total_amount=55, and memo mentions both entry types
- all 3 seeded ledger rows are status='paid' with paid_at set and
payout_batch_id linked to the new batch
- caught + fixed bigint-as-string pg gotcha (designerId comes back as
string '21' even though column is BIGINT; assertion now Number()-coerces
both sides)
Skips with exit 0 if PG is unreachable. Cleans up all seeded rows + CSV
file in a finally{} block so re-runs don't leak state.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
tests/marketplace/payouts-integration.test.js | 145 ++++++++++++++++++++++++++
1 file changed, 145 insertions(+)
diff --git a/tests/marketplace/payouts-integration.test.js b/tests/marketplace/payouts-integration.test.js
new file mode 100644
index 0000000..fd0d0e6
--- /dev/null
+++ b/tests/marketplace/payouts-integration.test.js
@@ -0,0 +1,145 @@
+// End-to-end integration test for payouts.exportPayoutBatch() — exercises the
+// real PG flow: insert designer + user + ledger rows, run export, assert batch
+// row created, CSV on disk with correct contents, ledger rows flipped to 'paid'
+// with payout_batch_id set. Cleans up everything after itself.
+//
+// Run: node tests/marketplace/payouts-integration.test.js
+//
+// Skips with exit 0 if PG is unreachable so this can live in any CI runner that
+// doesn't have the dw_unified DB wired up.
+
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+
+const SUFFIX = 'PAYOUT-IT-' + Date.now() + '-' + Math.random().toString(36).slice(2, 6);
+
+async function main() {
+ const { q, one, many, ping, pool } = require('../../src/marketplace/db');
+ const payouts = require('../../src/marketplace/payouts');
+
+ if (!(await ping())) {
+ console.log('⏭ PG unreachable — skipping payouts integration test');
+ process.exit(0);
+ }
+
+ let userId, designerId, csvPath;
+ const ledgerIds = [];
+ const failures = [];
+
+ try {
+ // ── seed: user + designer + 3 payable ledger rows
+ const u = await one(`
+ INSERT INTO mp_users (email, display_name, role)
+ VALUES ($1, $2, 'designer')
+ RETURNING id
+ `, [`payout-it-${SUFFIX.toLowerCase()}@test.invalid`, 'Payout IT ' + SUFFIX]);
+ userId = u.id;
+
+ const d = await one(`
+ INSERT INTO mp_designer_profiles
+ (user_id, display_name, slug, status, payout_method, commission_rate)
+ VALUES ($1, $2, $3, 'approved', 'ach', 15.00)
+ RETURNING id
+ `, [userId, 'Payout IT ' + SUFFIX, 'payout-it-' + SUFFIX.toLowerCase()]);
+ designerId = d.id;
+
+ for (const [basis, rate, amount, entryType] of [
+ [100, 15, 15.00, 'wallpaper_sale'],
+ [200, 15, 30.00, 'wallpaper_sale'],
+ [ 50, 20, 10.00, 'sample_credit'],
+ ]) {
+ const row = await one(`
+ INSERT INTO mp_commission_ledger
+ (designer_id, basis_amount, commission_rate, commission_amount, entry_type, status)
+ VALUES ($1, $2, $3, $4, $5, 'payable')
+ RETURNING id
+ `, [designerId, basis, rate, amount, entryType]);
+ ledgerIds.push(row.id);
+ }
+
+ // ── exercise: run exportPayoutBatch — should pick up our 3 rows + possibly
+ // others. We'll filter our assertions to our designer.
+ const result = await payouts.exportPayoutBatch({ notes: 'integration-test-' + SUFFIX });
+
+ // ── assertions
+ try {
+ assert.ok(!result.empty, 'export should not be empty when payable rows exist');
+ assert.ok(result.batch && result.batch.id, 'should return a batch row');
+ assert.ok(result.csvFilename && result.csvFilename.startsWith('payout-'), 'csv filename should follow batch label pattern');
+ assert.ok(fs.existsSync(result.csvPath), `csv file should exist on disk at ${result.csvPath}`);
+ csvPath = result.csvPath;
+
+ const csv = fs.readFileSync(result.csvPath, 'utf8');
+ const lines = csv.trim().split('\n');
+ assert.strictEqual(lines[0], 'designer_id,designer_email,payout_method,total_amount,memo', 'csv header must match spec');
+
+ // Find our designer's row in the CSV.
+ const ourLine = lines.slice(1).find(l => l.startsWith(Number(designerId) + ','));
+ assert.ok(ourLine, `csv should contain a row for designer ${designerId}; got:\n${csv}`);
+
+ const cols = ourLine.split(',');
+ assert.strictEqual(Number(cols[0]), Number(designerId), 'designer_id column');
+ assert.strictEqual(cols[1], `payout-it-${SUFFIX.toLowerCase()}@test.invalid`, 'designer_email column');
+ assert.strictEqual(cols[2], 'ach', 'payout_method column');
+ assert.strictEqual(Number(cols[3]), 55, `total_amount should be 15+30+10=55, got ${cols[3]}`);
+ // memo column is quoted (it contains a comma between entry types) — check it mentions both types
+ const memoJoined = ourLine.slice(ourLine.indexOf(cols[2]) + cols[2].length + 1 + cols[3].length + 1);
+ assert.ok(/sample_credit/.test(memoJoined), 'memo should mention sample_credit');
+ assert.ok(/wallpaper_sale/.test(memoJoined), 'memo should mention wallpaper_sale');
+ assert.ok(/3 entries/.test(memoJoined), 'memo should say "3 entries"');
+
+ // Verify ledger flipped to paid + payout_batch_id stamped.
+ const flipped = await many(`
+ SELECT id, status, paid_at, payout_batch_id
+ FROM mp_commission_ledger
+ WHERE id = ANY($1::bigint[])
+ `, [ledgerIds]);
+ assert.strictEqual(flipped.length, 3, 'should find all 3 seeded rows');
+ for (const row of flipped) {
+ assert.strictEqual(row.status, 'paid', `ledger row ${row.id} status should be 'paid'`);
+ assert.ok(row.paid_at, `ledger row ${row.id} should have paid_at set`);
+ assert.strictEqual(Number(row.payout_batch_id), Number(result.batch.id), `ledger row ${row.id} should be linked to batch`);
+ }
+
+ // Batch row sanity.
+ const batchRow = await one(`SELECT * FROM mp_payout_batches WHERE id=$1`, [result.batch.id]);
+ assert.strictEqual(batchRow.status, 'exported');
+ assert.ok(Number(batchRow.designer_count) >= 1);
+ assert.ok(Number(batchRow.total_amount) > 0);
+ assert.ok(batchRow.csv_export_path && batchRow.csv_export_path.includes('data/marketplace/payouts/'));
+ assert.strictEqual(batchRow.notes, 'integration-test-' + SUFFIX);
+
+ console.log('✓ payouts.exportPayoutBatch end-to-end (DB + CSV)');
+ } catch (err) {
+ failures.push(err);
+ console.error('✗ payouts.exportPayoutBatch end-to-end —', err.message);
+ }
+ } finally {
+ // ── cleanup: delete our ledger rows, the designer, the user, the CSV file
+ if (ledgerIds.length) {
+ await q(`DELETE FROM mp_commission_ledger WHERE id = ANY($1::bigint[])`, [ledgerIds]).catch(() => {});
+ }
+ if (designerId) {
+ await q(`DELETE FROM mp_designer_profiles WHERE id=$1`, [designerId]).catch(() => {});
+ }
+ if (userId) {
+ await q(`DELETE FROM mp_users WHERE id=$1`, [userId]).catch(() => {});
+ }
+ // Don't unlink the CSV — it's a real artifact other admins might want to see;
+ // but DO unlink if the test failed mid-run so we don't leave stale files.
+ // Actually, since this is a unique batch_label per run, always cleanup CSV.
+ if (csvPath && fs.existsSync(csvPath)) {
+ try { fs.unlinkSync(csvPath); } catch (_) {}
+ }
+ await pool.end().catch(() => {});
+ }
+
+ if (failures.length) process.exit(1);
+ console.log(`\n1/1 payouts integration tests passed`);
+}
+
+main().catch(err => {
+ console.error('integration test crashed:', err);
+ process.exit(1);
+});
← 2961365 Strip reserved-brand strings: BH/Beverly Hills source filena
·
back to Wallco Ai
·
brand: gucci viewer adopts universal corner-nav (bag/person/ 3b1a002 →