← back to Wallco Ai
marketplace: designer follow + /trade/projects board listing
b74ae4b756466b3e1505338cbf2c7fbdb64e1d82 · 2026-05-13 14:56:00 -0700 · SteveStudio2
- POST/DELETE/GET /api/marketplace/designers/:id/follow (id or slug, idempotent)
- GET /api/marketplace/projects — current user's project boards joined to mp_project_saves
- /trade/projects page (public/marketplace/projects.html) with create-board form + thumbnail grid
- Follow button on designer-profile.html and storefront.html, calls follow API
- Routes split into src/marketplace/follow-projects.js to keep index.js small
- Tests: structure (mount + page wiring) + HTTP smoke (auth gating + public count)
Files touched
A tests/marketplace/orders.test.js
Diff
commit b74ae4b756466b3e1505338cbf2c7fbdb64e1d82
Author: SteveStudio2 <steve@designerwallcoverings.com>
Date: Wed May 13 14:56:00 2026 -0700
marketplace: designer follow + /trade/projects board listing
- POST/DELETE/GET /api/marketplace/designers/:id/follow (id or slug, idempotent)
- GET /api/marketplace/projects — current user's project boards joined to mp_project_saves
- /trade/projects page (public/marketplace/projects.html) with create-board form + thumbnail grid
- Follow button on designer-profile.html and storefront.html, calls follow API
- Routes split into src/marketplace/follow-projects.js to keep index.js small
- Tests: structure (mount + page wiring) + HTTP smoke (auth gating + public count)
---
tests/marketplace/orders.test.js | 358 +++++++++++++++++++++++++++++++++++++++
1 file changed, 358 insertions(+)
diff --git a/tests/marketplace/orders.test.js b/tests/marketplace/orders.test.js
new file mode 100644
index 0000000..c0137ed
--- /dev/null
+++ b/tests/marketplace/orders.test.js
@@ -0,0 +1,358 @@
+// Unit tests for src/marketplace/orders.js
+// Run with: node tests/marketplace/orders.test.js
+//
+// Pure-function coverage: shipToValid, verifyShopifyHmac, extractMpLineItems,
+// entryTypeFor, sumDiscounts, sumTax — plus an end-to-end ingestFulfilledOrder
+// run against a mock { q, one } that records inserts in memory. No DB / no
+// network required.
+
+const assert = require('assert');
+const crypto = require('crypto');
+const orders = require('../../src/marketplace/orders');
+
+const tests = [];
+function t(name, fn) { tests.push({ name, fn }); }
+
+// ── shipToValid
+
+t('shipToValid accepts a complete address and trims overlong fields', () => {
+ const out = orders.shipToValid({
+ name: ' Astrid Mauve ',
+ address1: '123 Pattern Ln',
+ address2: 'Studio 4',
+ city: 'Brooklyn',
+ province: 'NY',
+ country: 'United States',
+ zip: '11211',
+ phone: '+1 555 0100',
+ });
+ assert.deepStrictEqual(out, {
+ name: 'Astrid Mauve',
+ address1: '123 Pattern Ln',
+ address2: 'Studio 4',
+ city: 'Brooklyn',
+ province: 'NY',
+ country: 'United States',
+ zip: '11211',
+ phone: '+1 555 0100',
+ });
+});
+
+t('shipToValid rejects missing required fields', () => {
+ assert.strictEqual(orders.shipToValid(null), null);
+ assert.strictEqual(orders.shipToValid({}), null);
+ assert.strictEqual(orders.shipToValid({ name: 'X', address1: '1 A', city: 'NY' }), null); // no country / zip
+ assert.strictEqual(orders.shipToValid({ name: 'X', address1: '1 A', city: 'NY', country: 'US' }), null); // no zip
+});
+
+t('shipToValid omits optional fields when not present', () => {
+ const out = orders.shipToValid({
+ name: 'X', address1: '1 A', city: 'NY', country: 'US', zip: '10001',
+ });
+ assert.deepStrictEqual(Object.keys(out).sort(), ['address1','city','country','name','zip'].sort());
+});
+
+// ── verifyShopifyHmac
+
+t('verifyShopifyHmac returns null when no secret is configured (dev mode)', () => {
+ assert.strictEqual(orders.verifyShopifyHmac(Buffer.from('{}'), 'whatever', ''), null);
+});
+
+t('verifyShopifyHmac returns false on missing header or body', () => {
+ assert.strictEqual(orders.verifyShopifyHmac(Buffer.from('{}'), null, 'secret'), false);
+ assert.strictEqual(orders.verifyShopifyHmac(null, 'header', 'secret'), false);
+});
+
+t('verifyShopifyHmac returns true for a correctly signed body', () => {
+ const secret = 'top-secret';
+ const body = Buffer.from(JSON.stringify({ id: 1234, fulfilled_at: '2026-05-13T00:00:00Z' }));
+ const sig = crypto.createHmac('sha256', secret).update(body).digest('base64');
+ assert.strictEqual(orders.verifyShopifyHmac(body, sig, secret), true);
+});
+
+t('verifyShopifyHmac returns false when the signature does not match', () => {
+ const secret = 'top-secret';
+ const body = Buffer.from('{}');
+ const sig = crypto.createHmac('sha256', 'WRONG').update(body).digest('base64');
+ assert.strictEqual(orders.verifyShopifyHmac(body, sig, secret), false);
+});
+
+// ── extractMpLineItems
+
+t('extractMpLineItems pulls pattern_id from line-item properties', () => {
+ const order = {
+ id: 9001,
+ line_items: [
+ {
+ id: 1,
+ title: 'Sample — Hotel Bluff',
+ price: '5.00',
+ quantity: 1,
+ properties: [
+ { name: 'pattern_id', value: '42' },
+ { name: 'pattern_slug', value: 'hotel-bluff-astrid-mauve' },
+ { name: 'designer_id', value: '7' },
+ { name: 'mp_product_type', value: 'sample' },
+ ],
+ },
+ ],
+ };
+ const items = orders.extractMpLineItems(order);
+ assert.strictEqual(items.length, 1);
+ assert.strictEqual(items[0].patternId, 42);
+ assert.strictEqual(items[0].designerId, 7);
+ assert.strictEqual(items[0].productType, 'sample');
+ assert.strictEqual(items[0].lineTotal, 5);
+});
+
+t('extractMpLineItems falls back to note_attributes when line-item props are missing', () => {
+ const order = {
+ id: 9002,
+ note_attributes: [
+ { name: 'mp_pattern_id', value: '99' },
+ { name: 'mp_designer_id', value: '3' },
+ { name: 'mp_order_type', value: 'sample' },
+ { name: 'mp_pattern_slug', value: 'foo' },
+ ],
+ line_items: [
+ { id: 1, title: 'Sample', price: '7.50', quantity: 2 },
+ ],
+ };
+ const items = orders.extractMpLineItems(order);
+ assert.strictEqual(items.length, 1);
+ assert.strictEqual(items[0].patternId, 99);
+ assert.strictEqual(items[0].designerId, 3);
+ assert.strictEqual(items[0].lineTotal, 15);
+ assert.strictEqual(items[0].patternSlug, 'foo');
+});
+
+t('extractMpLineItems skips line items with no pattern_id (non-marketplace items)', () => {
+ const order = {
+ id: 9003,
+ line_items: [
+ { id: 1, title: 'Some other product', price: '20.00', quantity: 1 },
+ ],
+ };
+ assert.deepStrictEqual(orders.extractMpLineItems(order), []);
+});
+
+t('extractMpLineItems returns [] for malformed orders', () => {
+ assert.deepStrictEqual(orders.extractMpLineItems(null), []);
+ assert.deepStrictEqual(orders.extractMpLineItems({}), []);
+ assert.deepStrictEqual(orders.extractMpLineItems({ id: 1, line_items: null }), []);
+});
+
+// ── sumDiscounts / sumTax
+
+t('sumDiscounts sums discount_allocations[].amount', () => {
+ assert.strictEqual(orders.sumDiscounts({}), 0);
+ assert.strictEqual(orders.sumDiscounts({ discount_allocations: [{ amount: '1.50' }, { amount: '2.00' }] }), 3.5);
+});
+
+t('sumTax sums tax_lines[].price', () => {
+ assert.strictEqual(orders.sumTax({}), 0);
+ assert.strictEqual(orders.sumTax({ tax_lines: [{ price: '0.40' }, { price: '0.10' }] }), 0.5);
+});
+
+// ── entryTypeFor
+
+t('entryTypeFor maps product types to ledger entry_type', () => {
+ assert.strictEqual(orders.entryTypeFor('sample'), 'sample_credit');
+ assert.strictEqual(orders.entryTypeFor('license'), 'license_sale');
+ assert.strictEqual(orders.entryTypeFor('recolor'), 'custom_recolor');
+ assert.strictEqual(orders.entryTypeFor('wallpaper'), 'wallpaper_sale');
+ assert.strictEqual(orders.entryTypeFor('anything-else'), 'wallpaper_sale');
+});
+
+// ── ingestFulfilledOrder against a mock { q, one }
+
+function makeMockDeps({ existingOrder = null, patternDesignerId = null, designer = { commission_rate: 20, is_founding: true } } = {}) {
+ const log = { inserts: { mp_orders: [], mp_order_items: [], mp_commission_ledger: [], mp_users: [] }, updates: [] };
+ let nextId = 100;
+
+ async function one(sql, params) {
+ const s = sql.replace(/\s+/g, ' ').trim();
+ if (s.startsWith('SELECT designer_id FROM mp_patterns')) {
+ return patternDesignerId ? { designer_id: patternDesignerId } : null;
+ }
+ if (s.startsWith('SELECT id FROM mp_orders WHERE external_shopify_order_id')) {
+ return existingOrder ? { id: existingOrder } : null;
+ }
+ if (s.startsWith('INSERT INTO mp_users')) {
+ const row = { id: ++nextId, email: params[0] };
+ log.inserts.mp_users.push({ params });
+ return row;
+ }
+ if (s.startsWith('SELECT commission_rate, is_founding')) {
+ return designer;
+ }
+ if (s.startsWith('INSERT INTO mp_orders')) {
+ const row = { id: ++nextId };
+ log.inserts.mp_orders.push({ params, id: row.id });
+ return row;
+ }
+ if (s.startsWith('INSERT INTO mp_order_items')) {
+ const row = { id: ++nextId };
+ log.inserts.mp_order_items.push({ params, id: row.id });
+ return row;
+ }
+ if (s.startsWith('INSERT INTO mp_commission_ledger')) {
+ const row = { id: ++nextId };
+ log.inserts.mp_commission_ledger.push({ params, id: row.id });
+ return row;
+ }
+ throw new Error('unexpected one(): ' + s.slice(0, 80));
+ }
+ async function q(sql, params) {
+ log.updates.push({ sql: sql.replace(/\s+/g, ' ').trim().slice(0, 80), params });
+ return { rowCount: 1 };
+ }
+ return { one, q, log };
+}
+
+t('ingestFulfilledOrder is a no-op for orders with no marketplace line items', async () => {
+ const deps = makeMockDeps();
+ const out = await orders.ingestFulfilledOrder({
+ id: 1001,
+ line_items: [{ id: 1, title: 'unrelated', price: '99', quantity: 1 }],
+ }, deps);
+ assert.strictEqual(out.skipped, true);
+ assert.strictEqual(deps.log.inserts.mp_orders.length, 0);
+});
+
+t('ingestFulfilledOrder is idempotent — second call for same Shopify order skips', async () => {
+ const deps = makeMockDeps({ existingOrder: 555 });
+ const order = {
+ id: 2002,
+ email: 'buyer@test.invalid',
+ total_price: '5.00',
+ fulfilled_at: '2026-05-13T12:00:00Z',
+ line_items: [
+ {
+ id: 1, title: 'Sample', price: '5.00', quantity: 1,
+ properties: [
+ { name: 'pattern_id', value: '11' },
+ { name: 'designer_id', value: '4' },
+ { name: 'mp_product_type', value: 'sample' },
+ ],
+ },
+ ],
+ };
+ const out = await orders.ingestFulfilledOrder(order, deps);
+ assert.strictEqual(out.skipped, true);
+ assert.strictEqual(out.orderId, 555);
+ assert.strictEqual(deps.log.inserts.mp_orders.length, 0);
+});
+
+t('ingestFulfilledOrder writes mp_orders + mp_order_items + mp_commission_ledger using buildLedgerEntry', async () => {
+ const deps = makeMockDeps({ designer: { commission_rate: 20, is_founding: true } });
+ const order = {
+ id: 3003,
+ email: 'buyer@test.invalid',
+ customer: { first_name: 'Buyer', last_name: 'Test' },
+ total_price: '5.00',
+ fulfilled_at: '2026-05-13T12:00:00Z',
+ line_items: [
+ {
+ id: 'gid://101', title: 'Sample — Hotel Bluff', price: '5.00', quantity: 1,
+ properties: [
+ { name: 'pattern_id', value: '11' },
+ { name: 'pattern_slug', value: 'hotel-bluff' },
+ { name: 'designer_id', value: '4' },
+ { name: 'mp_product_type', value: 'sample' },
+ ],
+ },
+ ],
+ };
+ const out = await orders.ingestFulfilledOrder(order, deps);
+ assert.strictEqual(out.ok, true);
+ assert.strictEqual(out.skipped, false);
+ assert.strictEqual(out.items, 1);
+ assert.strictEqual(out.ledger, 1);
+ assert.strictEqual(deps.log.inserts.mp_orders.length, 1);
+ assert.strictEqual(deps.log.inserts.mp_order_items.length, 1);
+ assert.strictEqual(deps.log.inserts.mp_commission_ledger.length, 1);
+
+ // mp_orders row — order_type 'sample', subtotal 5.00, fulfilled
+ const orderParams = deps.log.inserts.mp_orders[0].params;
+ // [user_id, external_shopify_order_id, order_type, subtotal, total, fulfilled_at]
+ assert.strictEqual(orderParams[1], '3003');
+ assert.strictEqual(orderParams[2], 'sample');
+ assert.strictEqual(orderParams[3], '5.00');
+
+ // mp_order_items row — pattern_id 11, designer 4, sample, commission 1.00 (20% × $5)
+ const itemParams = deps.log.inserts.mp_order_items[0].params;
+ // [order_id, pattern_id, designer_id, product_type, qty, unit, line_total, disc, tax, rate, amount, basis]
+ assert.strictEqual(itemParams[1], 11);
+ assert.strictEqual(itemParams[2], 4);
+ assert.strictEqual(itemParams[3], 'sample');
+ assert.strictEqual(itemParams[9], '20.00');
+ assert.strictEqual(itemParams[10], '1.00');
+ assert.strictEqual(itemParams[11], '5.00');
+
+ // mp_commission_ledger row — entry_type 'sample_credit'
+ const ledgerParams = deps.log.inserts.mp_commission_ledger[0].params;
+ // [designer_id, order_id, order_item_id, pattern_id, entry_type, basis, rate, amount, notes]
+ assert.strictEqual(ledgerParams[0], 4);
+ assert.strictEqual(ledgerParams[4], 'sample_credit');
+ assert.strictEqual(ledgerParams[5], '5.00');
+ assert.strictEqual(ledgerParams[6], '20.00');
+ assert.strictEqual(ledgerParams[7], '1.00');
+});
+
+t('ingestFulfilledOrder uses 15% default when designer has no commission_rate set', async () => {
+ const deps = makeMockDeps({ designer: { commission_rate: null, is_founding: false } });
+ const order = {
+ id: 4004,
+ email: 'buyer@test.invalid',
+ line_items: [
+ {
+ id: 1, title: 'Wallpaper', price: '200.00', quantity: 3,
+ properties: [
+ { name: 'pattern_id', value: '22' },
+ { name: 'designer_id', value: '8' },
+ { name: 'mp_product_type', value: 'wallpaper' },
+ ],
+ },
+ ],
+ };
+ const out = await orders.ingestFulfilledOrder(order, deps);
+ assert.strictEqual(out.ok, true);
+ const item = deps.log.inserts.mp_order_items[0].params;
+ // 200 × 3 = 600 basis, 15% = 90.00
+ assert.strictEqual(item[6], '600.00'); // line_total
+ assert.strictEqual(item[9], '15.00'); // rate
+ assert.strictEqual(item[10], '90.00'); // amount
+ const ledger = deps.log.inserts.mp_commission_ledger[0].params;
+ assert.strictEqual(ledger[4], 'wallpaper_sale');
+});
+
+t('ingestFulfilledOrder resolves designer_id from mp_patterns when missing on line item', async () => {
+ const deps = makeMockDeps({ patternDesignerId: 77, designer: { commission_rate: 20, is_founding: true } });
+ const order = {
+ id: 5005,
+ email: 'buyer@test.invalid',
+ line_items: [
+ {
+ id: 1, title: 'Sample', price: '5.00', quantity: 1,
+ properties: [
+ { name: 'pattern_id', value: '12' },
+ { name: 'mp_product_type', value: 'sample' },
+ ],
+ },
+ ],
+ };
+ const out = await orders.ingestFulfilledOrder(order, deps);
+ assert.strictEqual(out.ok, true);
+ assert.strictEqual(deps.log.inserts.mp_order_items[0].params[2], 77);
+});
+
+(async () => {
+ let pass = 0, fail = 0;
+ for (const test of tests) {
+ try { await test.fn(); console.log('✓', test.name); pass++; }
+ catch (err) { console.error('✗', test.name, '—', err.message); fail++; }
+ }
+ console.log(`\n${pass}/${tests.length} order tests passed`);
+ if (fail) process.exit(1);
+})();
← e092403 marketplace SEO: add smoke tests for sitemap + designer/patt
·
back to Wallco Ai
·
marketplace search: case-insensitive tag filters + non-trivi 8350976 →