import assert from 'node:assert/strict'; import { MemberProfileError, MemberProfileService } from '../dist/wallets/member-profile-service.js'; const customers = [ { id: '21', tenantId: '7', userType: 'CUSTOMER', status: 'ACTIVE', nickname: 'Alice', phone: '13800138000', createdAt: new Date('2026-06-01T00:00:00.000Z'), lastLoginAt: new Date('2026-06-20T00:00:00.000Z'), deleted: false }, { id: '22', tenantId: '7', userType: 'CUSTOMER', status: 'ACTIVE', nickname: 'Bob', phone: '13900139000', createdAt: new Date('2026-06-02T00:00:00.000Z'), lastLoginAt: null, deleted: false }, { id: '23', tenantId: '8', userType: 'CUSTOMER', status: 'ACTIVE', nickname: 'OtherTenant', phone: '13700137000', createdAt: new Date('2026-06-03T00:00:00.000Z'), lastLoginAt: null, deleted: false } ]; const wallets = [ { tenantId: '7', userId: '21', storeId: '11', status: 'ACTIVE', cash: 10_000, gift: 2_000 }, { tenantId: '7', userId: '21', storeId: null, status: 'ACTIVE', cash: 5_000, gift: 500 }, { tenantId: '7', userId: '22', storeId: '12', status: 'ACTIVE', cash: 1_000, gift: 0 } ]; const coupons = [ { tenantId: '7', userId: '21', status: 'AVAILABLE' }, { tenantId: '7', userId: '21', status: 'FROZEN' }, { tenantId: '7', userId: '22', status: 'USED' } ]; const packages = [ { tenantId: '7', userId: '21', status: 'ACTIVE', remainingMinutes: 120, remainingAmountCents: 3_000 }, { tenantId: '7', userId: '21', status: 'FROZEN', remainingMinutes: 60, remainingAmountCents: 2_000 } ]; const recharges = [ { tenantId: '7', userId: '21', status: 'CREDITED', payAmountCents: 10_000, giftAmountCents: 2_000, creditedAt: new Date('2026-06-21T00:00:00.000Z') }, { tenantId: '7', userId: '21', status: 'PENDING_PAYMENT', payAmountCents: 5_000, giftAmountCents: 500, creditedAt: null } ]; const ledger = [ { id: '1002', tenantId: '7', userId: '21', storeId: '11', businessType: 'ORDER', businessId: 'order-1', entryType: 'CONSUME', cashDeltaCents: -200, giftDeltaCents: -300, cashBalanceAfterCents: 14_800, giftBalanceAfterCents: 2_200, createdAt: new Date('2026-06-22T00:00:00.000Z') }, { id: '1001', tenantId: '7', userId: '21', storeId: '11', businessType: 'RECHARGE', businessId: 'recharge-1', entryType: 'RECHARGE', cashDeltaCents: 10_000, giftDeltaCents: 2_000, cashBalanceAfterCents: 15_000, giftBalanceAfterCents: 2_500, createdAt: new Date('2026-06-21T00:00:00.000Z') } ]; const orders = [ { id: '301', tenantId: '7', userId: '21', status: 'COMPLETED', paidAmountCents: 12_000, createdAt: new Date('2026-06-23T00:00:00.000Z') }, { id: '302', tenantId: '7', userId: '21', status: 'CANCELLED', paidAmountCents: 0, createdAt: new Date('2026-06-24T00:00:00.000Z') } ]; function createService() { const pool = { async execute(sql, params) { if (sql.includes('COUNT(DISTINCT u.id)')) { return [[{ total: queryMembers(sql, params).length }], []]; } if (sql.includes('FROM qipai_users u') && sql.includes('ORDER BY u.id DESC')) { return [queryMembers(sql, params).sort((a, b) => Number(b.id) - Number(a.id)), []]; } if (sql.includes('FROM qipai_users u') && sql.includes('LIMIT 1')) { const found = queryMembers(sql, params).find((user) => user.id === String(params[1])); return [found ? [found] : [], []]; } if (sql.includes('FROM qipai_wallet_accounts')) { const tenantId = String(params[0]); const userId = String(params[1]); const rows = wallets.filter((item) => item.tenantId === tenantId && item.userId === userId && item.status === 'ACTIVE' ); return [[{ accountCount: rows.length, cashBalanceCents: rows.reduce((sum, item) => sum + item.cash, 0), giftBalanceCents: rows.reduce((sum, item) => sum + item.gift, 0) }], []]; } if (sql.includes('FROM qipai_coupon_grants c')) { const tenantId = String(params[8]); const userId = String(params[9]); const memberCoupons = coupons.filter((item) => item.tenantId === tenantId && item.userId === userId ); const memberPackages = packages.filter((item) => item.tenantId === tenantId && item.userId === userId ); return [[{ availableCoupons: memberCoupons.filter((item) => item.status === 'AVAILABLE').length, frozenCoupons: memberCoupons.filter((item) => item.status === 'FROZEN').length, activePackages: memberPackages.filter((item) => item.status === 'ACTIVE').length, frozenPackages: memberPackages.filter((item) => item.status === 'FROZEN').length, packageMinutes: memberPackages .filter((item) => ['ACTIVE', 'FROZEN'].includes(item.status)) .reduce((sum, item) => sum + item.remainingMinutes, 0), packageAmountCents: memberPackages .filter((item) => ['ACTIVE', 'FROZEN'].includes(item.status)) .reduce((sum, item) => sum + item.remainingAmountCents, 0) }], []]; } if (sql.includes('FROM qipai_recharge_orders')) { const tenantId = String(params[0]); const userId = String(params[1]); const rows = recharges.filter((item) => item.tenantId === tenantId && item.userId === userId); const credited = rows.filter((item) => item.status === 'CREDITED'); return [[{ rechargeOrderCount: rows.length, creditedRechargeCount: credited.length, creditedRechargeCents: credited.reduce((sum, item) => sum + item.payAmountCents, 0), giftedRechargeCents: credited.reduce((sum, item) => sum + item.giftAmountCents, 0), lastRechargeAt: credited[0]?.creditedAt ?? null }], []]; } if (sql.includes('FROM qipai_orders o')) { const userId = String(params[0]); const tenantId = String(params[1]); const rows = orders.filter((item) => item.tenantId === tenantId && item.userId === userId); const paid = rows.filter((item) => ['PAID', 'IN_USE', 'COMPLETED'].includes(item.status)); return [[{ orderCount: rows.length, paidOrderCount: paid.length, paidAmountCents: paid.reduce((sum, item) => sum + item.paidAmountCents, 0), lastOrderAt: rows.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())[0]?.createdAt ?? null }], []]; } if (sql.includes('FROM qipai_wallet_ledger_entries')) { const rows = ledger .filter((item) => item.tenantId === String(params[0]) && item.userId === String(params[1])) .sort((a, b) => Number(b.id) - Number(a.id)) .slice(0, Number(params[2])); return [rows.map((item) => ({ id: item.id, storeId: item.storeId, businessType: item.businessType, businessId: item.businessId, entryType: item.entryType, cashDeltaCents: item.cashDeltaCents, giftDeltaCents: item.giftDeltaCents, cashBalanceAfterCents: item.cashBalanceAfterCents, giftBalanceAfterCents: item.giftBalanceAfterCents, createdAt: item.createdAt })), []]; } throw new Error(`Unexpected SQL: ${sql}`); } }; return new MemberProfileService(pool); } function queryMembers(sql, params) { if (sql.includes('1 = 0')) return []; const tenantId = String(params[0]); const statusFilter = sql.includes('u.status = ?') ? params[1] : null; const hasStoreScope = sql.includes('qipai_wallet_accounts wa'); const storeParams = hasStoreScope ? params.slice(statusFilter ? 2 : 1).filter((value) => /^\d+$/.test(String(value))) : []; return customers.filter((user) => { if (user.tenantId !== tenantId || user.userType !== 'CUSTOMER' || user.deleted) return false; if (statusFilter && user.status !== statusFilter) return false; if (hasStoreScope) { return wallets.some((wallet) => wallet.tenantId === tenantId && wallet.userId === user.id && storeParams.includes(wallet.storeId) ); } return true; }).map((user) => ({ id: user.id, status: user.status, nickname: user.nickname, phone: user.phone, createdAt: user.createdAt, lastLoginAt: user.lastLoginAt })); } const tenantAdmin = { tenantId: '7', userId: '1', access: { roles: ['TENANT_ADMIN'], capabilities: ['tenant.manage'], storeIds: [] } }; const storeReader = { tenantId: '7', userId: '2', access: { roles: ['STORE_ADMIN'], capabilities: ['user.read'], storeIds: ['11'] } }; const forbiddenReader = { tenantId: '7', userId: '3', access: { roles: ['STAFF'], capabilities: ['user.read'], storeIds: [] } }; { const service = createService(); const list = await service.listMembers({ actor: tenantAdmin, page: 1, pageSize: 20 }); assert.equal(list.total, 2); assert.equal(list.items[0].memberId, '22'); assert.equal(list.items[1].wallet.totalBalanceCents, 17_500); assert.equal(list.items[1].benefits.availableCoupons, 1); assert.equal(list.items[1].benefits.frozenPackages, 1); assert.equal(list.items[1].recharge.creditedRechargeCents, 10_000); assert.equal(list.items[1].orders.orderCount, 2); assert.equal(list.items[1].orders.paidAmountCents, 12_000); assert.equal(list.items[1].maskedPhone, '138****8000'); } { const service = createService(); const member = await service.getMember({ actor: tenantAdmin, memberId: '21' }); assert.equal(member.recentLedger.length, 2); assert.equal(member.recentLedger[0].ledgerId, '1002'); assert.equal(member.wallet.cashBalanceCents, 15_000); assert.equal(member.benefits.packageMinutes, 180); } { const service = createService(); const scoped = await service.listMembers({ actor: storeReader, page: 1, pageSize: 20 }); assert.equal(scoped.total, 1); assert.equal(scoped.items[0].memberId, '21'); await assert.rejects( () => service.getMember({ actor: storeReader, memberId: '22' }), (error) => error instanceof MemberProfileError && error.code === 'MEMBER_NOT_FOUND' ); } { const service = createService(); const scoped = await service.listMembers({ actor: forbiddenReader, page: 1, pageSize: 20 }); assert.equal(scoped.total, 0); } console.log('PASS: M07-D member profiles aggregate wallets, benefits, recharge and scoped access.');