Compose-ready workspace: packages/zappier (rate card, portal, Stripe), packages/verae-zapier-middleware (timestamp + NATS), packages/verae-zapier (CLI app), vendor/zapier-platform, and research/zapier vendor corpus. Gate 0 structure checks pass. Product code and research are not yet wired.
116 lines
4.7 KiB
TypeScript
116 lines
4.7 KiB
TypeScript
import Database from 'better-sqlite3';
|
|
import { Customer } from '../src/auth';
|
|
import { SqliteCustomerRepo } from '../src/db/customer-repo';
|
|
|
|
const customer = (apiKey: string): Customer => ({
|
|
id: 'cust_1',
|
|
name: 'Ada',
|
|
tierId: 'pro',
|
|
apiKey,
|
|
stripeCustomerId: 'cus_123',
|
|
});
|
|
|
|
describe('SqliteCustomerRepo', () => {
|
|
it('finds a customer by API key', () => {
|
|
const repo = new SqliteCustomerRepo(new Database(':memory:'), [customer('key-ada')]);
|
|
expect(repo.findByApiKey('key-ada')?.tierId).toBe('pro');
|
|
});
|
|
|
|
it('returns undefined for an unknown key', () => {
|
|
const repo = new SqliteCustomerRepo(new Database(':memory:'), [customer('key-ada')]);
|
|
expect(repo.findByApiKey('wrong')).toBeUndefined();
|
|
});
|
|
|
|
it('lists all customers with their Stripe ids', () => {
|
|
const repo = new SqliteCustomerRepo(new Database(':memory:'), [customer('key-ada')]);
|
|
const all = repo.list();
|
|
expect(all).toHaveLength(1);
|
|
expect(all[0].stripeCustomerId).toBe('cus_123');
|
|
});
|
|
|
|
it('seeds only when the table is empty', () => {
|
|
const db = new Database(':memory:');
|
|
new SqliteCustomerRepo(db, [customer('key-ada')]);
|
|
const again = new SqliteCustomerRepo(db, [customer('key-other')]);
|
|
expect(again.list().map((c) => c.apiKey)).toEqual(['key-ada']);
|
|
});
|
|
|
|
it('save() upserts including the multiplier override', () => {
|
|
const repo = new SqliteCustomerRepo(new Database(':memory:'), [customer('key-ada')]);
|
|
repo.save({ ...customer('key-ada'), tierId: 'business', multiplierOverride: 0.4 });
|
|
const updated = repo.findByApiKey('key-ada');
|
|
expect(updated?.tierId).toBe('business');
|
|
expect(updated?.multiplierOverride).toBe(0.4);
|
|
expect(repo.list()).toHaveLength(1);
|
|
});
|
|
|
|
it('round-trips billingType and email', () => {
|
|
const repo = new SqliteCustomerRepo(new Database(':memory:'), [customer('key-ada')]);
|
|
repo.save({ ...customer('key-ada'), billingType: 'purchase_order', email: 'ada@example.com' });
|
|
const updated = repo.findByApiKey('key-ada');
|
|
expect(updated?.billingType).toBe('purchase_order');
|
|
expect(updated?.email).toBe('ada@example.com');
|
|
});
|
|
|
|
it('migrates a legacy table, defaulting billingType to stripe', () => {
|
|
const db = new Database(':memory:');
|
|
db.exec(`CREATE TABLE customers (
|
|
id TEXT PRIMARY KEY, name TEXT NOT NULL, tier_id TEXT NOT NULL,
|
|
api_key TEXT NOT NULL UNIQUE, stripe_customer_id TEXT, multiplier_override REAL
|
|
)`);
|
|
db.prepare(
|
|
`INSERT INTO customers (id, name, tier_id, api_key) VALUES ('cust_1', 'Ada', 'pro', 'key-ada')`,
|
|
).run();
|
|
const repo = new SqliteCustomerRepo(db);
|
|
const migrated = repo.findByApiKey('key-ada');
|
|
expect(migrated?.billingType).toBe('stripe');
|
|
expect(migrated?.email).toBeUndefined();
|
|
// and new columns are writable after migration
|
|
repo.save({ ...migrated!, billingType: 'purchase_order' });
|
|
expect(repo.findByApiKey('key-ada')?.billingType).toBe('purchase_order');
|
|
});
|
|
|
|
it('finds a customer by email (case-insensitive)', () => {
|
|
const repo = new SqliteCustomerRepo(new Database(':memory:'), [
|
|
{ ...customer('key-ada'), email: 'Ada@Example.com' },
|
|
]);
|
|
expect(repo.findByEmail('ada@example.com')?.id).toBe('cust_1');
|
|
expect(repo.findByEmail('nobody@example.com')).toBeUndefined();
|
|
});
|
|
|
|
it('round-trips portal identity fields', () => {
|
|
const repo = new SqliteCustomerRepo(new Database(':memory:'), [customer('key-ada')]);
|
|
repo.save({
|
|
...customer('key-ada'),
|
|
passwordHash: 'scrypt:16384:8:1:c2FsdA==:aGFzaA==',
|
|
totpSecret: 'ABC234',
|
|
totpEnabled: true,
|
|
balanceCents: 2500,
|
|
emailInvoicing: true,
|
|
});
|
|
const updated = repo.findByApiKey('key-ada');
|
|
expect(updated?.passwordHash).toBe('scrypt:16384:8:1:c2FsdA==:aGFzaA==');
|
|
expect(updated?.totpSecret).toBe('ABC234');
|
|
expect(updated?.totpEnabled).toBe(true);
|
|
expect(updated?.balanceCents).toBe(2500);
|
|
expect(updated?.emailInvoicing).toBe(true);
|
|
});
|
|
|
|
it('defaults portal identity fields for legacy rows', () => {
|
|
const db = new Database(':memory:');
|
|
db.exec(`CREATE TABLE customers (
|
|
id TEXT PRIMARY KEY, name TEXT NOT NULL, tier_id TEXT NOT NULL,
|
|
api_key TEXT NOT NULL UNIQUE, stripe_customer_id TEXT, multiplier_override REAL
|
|
)`);
|
|
db.prepare(
|
|
`INSERT INTO customers (id, name, tier_id, api_key) VALUES ('cust_1', 'Ada', 'pro', 'key-ada')`,
|
|
).run();
|
|
const repo = new SqliteCustomerRepo(db);
|
|
const migrated = repo.findByApiKey('key-ada');
|
|
expect(migrated?.passwordHash).toBeUndefined();
|
|
expect(migrated?.totpSecret).toBeUndefined();
|
|
expect(migrated?.totpEnabled).toBe(false);
|
|
expect(migrated?.balanceCents).toBe(0);
|
|
expect(migrated?.emailInvoicing).toBe(false);
|
|
});
|
|
});
|