218 lines
8.7 KiB
TypeScript
218 lines
8.7 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { buyBack, changeLoyaltyProgram, recordLostRequest, recordSatisfiedRequest, sell, trade } from '../../src/sim/transactions';
|
|
import { createNewGame } from '../../src/sim/state';
|
|
import { backroomQty, getSlot } from '../../src/sim/inventory';
|
|
import { issueCard, setProgram } from '../../src/sim/loyalty';
|
|
import { makeCustomer, stock, testCatalog } from './helpers';
|
|
|
|
const catalog = testCatalog();
|
|
const tetrisNew = { gameId: 'tetris', condition: 'new' as const };
|
|
const tetrisLoose = { gameId: 'tetris', condition: 'loose' as const };
|
|
const smlComplete = { gameId: 'sml', condition: 'complete' as const };
|
|
|
|
describe('sell', () => {
|
|
it('takes the item, cashes the price and records the sale', () => {
|
|
const s = createNewGame(1);
|
|
stock(s, 'slot-0', tetrisNew, 2);
|
|
const cash = s.cash;
|
|
const result = sell(s, catalog, 'slot-0', makeCustomer());
|
|
expect(result.priceCents).toBe(3034);
|
|
expect(s.cash).toBe(cash + 3034);
|
|
expect(getSlot(s.inventory, 'slot-0').qty).toBe(1);
|
|
expect(s.day.salesCount).toBe(1);
|
|
expect(s.day.revenueCents).toBe(3034);
|
|
expect(s.reputation).toBeCloseTo(20.1);
|
|
});
|
|
|
|
it('stamps the card of a card holder', () => {
|
|
const s = createNewGame(1);
|
|
const card = issueCard(s.loyalty);
|
|
stock(s, 'slot-0', tetrisNew, 1);
|
|
sell(s, catalog, 'slot-0', makeCustomer({ cardId: card.id }));
|
|
expect(card.stamps).toEqual([1]);
|
|
});
|
|
|
|
it('applies a free used game reward', () => {
|
|
const s = createNewGame(1);
|
|
const card = issueCard(s.loyalty);
|
|
card.pendingRewards.push({ kind: 'free_used_game', maxValueCents: 2000 });
|
|
stock(s, 'slot-0', tetrisLoose, 1);
|
|
const cash = s.cash;
|
|
const result = sell(s, catalog, 'slot-0', makeCustomer({ cardId: card.id }));
|
|
expect(result.priceCents).toBe(0);
|
|
expect(result.rewardUsed).toEqual({ kind: 'free_used_game', maxValueCents: 2000 });
|
|
expect(s.cash).toBe(cash);
|
|
expect(card.pendingRewards).toEqual([]);
|
|
});
|
|
|
|
it('applies a percentage discount', () => {
|
|
const s = createNewGame(1);
|
|
const card = issueCard(s.loyalty);
|
|
card.pendingRewards.push({ kind: 'discount_percent', percent: 50 });
|
|
stock(s, 'slot-0', tetrisNew, 1);
|
|
expect(sell(s, catalog, 'slot-0', makeCustomer({ cardId: card.id })).priceCents).toBe(1517);
|
|
});
|
|
|
|
it('spends the card voucher', () => {
|
|
const s = createNewGame(1);
|
|
const card = issueCard(s.loyalty);
|
|
card.voucherCents = 1000;
|
|
stock(s, 'slot-0', tetrisNew, 1);
|
|
const cash = s.cash;
|
|
expect(sell(s, catalog, 'slot-0', makeCustomer({ cardId: card.id })).priceCents).toBe(2034);
|
|
expect(s.cash).toBe(cash + 2034);
|
|
expect(card.voucherCents).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('buyBack', () => {
|
|
it('is refused below what the customer accepts', () => {
|
|
const s = createNewGame(1);
|
|
const cash = s.cash;
|
|
expect(buyBack(s, catalog, makeCustomer({ intent: 'buy_back', offered: smlComplete }), 796)).toEqual({ accepted: false });
|
|
expect(s.cash).toBe(cash);
|
|
});
|
|
|
|
it('buys the game into the backroom when accepted', () => {
|
|
const s = createNewGame(1);
|
|
const cash = s.cash;
|
|
expect(buyBack(s, catalog, makeCustomer({ intent: 'buy_back', offered: smlComplete }), 797)).toEqual({ accepted: true });
|
|
expect(s.cash).toBe(cash - 797);
|
|
expect(backroomQty(s.inventory, smlComplete)).toBe(1);
|
|
expect(s.day.buyBacks).toBe(1);
|
|
expect(s.day.expensesCents).toBe(797);
|
|
});
|
|
|
|
it('throws when the shop cannot pay', () => {
|
|
const s = createNewGame(1);
|
|
s.cash = 100;
|
|
expect(() => buyBack(s, catalog, makeCustomer({ intent: 'buy_back', offered: smlComplete }), 797)).toThrow('Not enough cash');
|
|
});
|
|
|
|
it('stamps buy_back cards', () => {
|
|
const s = createNewGame(1);
|
|
setProgram(s.loyalty, [{ stampType: 'buy_back', slots: 5, reward: { kind: 'free_trade' } }]);
|
|
const card = issueCard(s.loyalty);
|
|
buyBack(s, catalog, makeCustomer({ intent: 'buy_back', offered: smlComplete, cardId: card.id }), 797);
|
|
expect(card.stamps).toEqual([1]);
|
|
});
|
|
});
|
|
|
|
describe('trade', () => {
|
|
it('is refused below the trade value the customer expects', () => {
|
|
const s = createNewGame(1);
|
|
stock(s, 'slot-0', tetrisNew, 1);
|
|
const result = trade(s, catalog, makeCustomer({ intent: 'trade', offered: smlComplete }), 'slot-0', 1138);
|
|
expect(result.accepted).toBe(false);
|
|
expect(getSlot(s.inventory, 'slot-0').qty).toBe(1);
|
|
});
|
|
|
|
it('makes the customer pay the difference', () => {
|
|
const s = createNewGame(1);
|
|
stock(s, 'slot-0', tetrisNew, 1);
|
|
const cash = s.cash;
|
|
const result = trade(s, catalog, makeCustomer({ intent: 'trade', offered: smlComplete }), 'slot-0', 1139);
|
|
expect(result).toEqual({ accepted: true, customerPaysCents: 1895, shopPaysCents: 0, voucherGivenCents: 0 });
|
|
expect(s.cash).toBe(cash + 1895);
|
|
expect(backroomQty(s.inventory, smlComplete)).toBe(1);
|
|
expect(getSlot(s.inventory, 'slot-0').qty).toBe(0);
|
|
expect(s.day.trades).toBe(1);
|
|
});
|
|
|
|
it('pays the excess in cash to customers without a card', () => {
|
|
const s = createNewGame(1);
|
|
stock(s, 'slot-0', tetrisNew, 1);
|
|
const cash = s.cash;
|
|
const result = trade(s, catalog, makeCustomer({ intent: 'trade', offered: smlComplete }), 'slot-0', 4000);
|
|
expect(result).toEqual({ accepted: true, customerPaysCents: 0, shopPaysCents: 966, voucherGivenCents: 0 });
|
|
expect(s.cash).toBe(cash - 966);
|
|
});
|
|
|
|
it('gives the excess as a voucher to card holders', () => {
|
|
const s = createNewGame(1);
|
|
const card = issueCard(s.loyalty);
|
|
stock(s, 'slot-0', tetrisNew, 1);
|
|
const cash = s.cash;
|
|
const result = trade(s, catalog, makeCustomer({ intent: 'trade', offered: smlComplete, cardId: card.id }), 'slot-0', 4000);
|
|
expect(result.voucherGivenCents).toBe(966);
|
|
expect(card.voucherCents).toBe(966);
|
|
expect(s.cash).toBe(cash);
|
|
});
|
|
|
|
it('waives the difference with a free trade reward', () => {
|
|
const s = createNewGame(1);
|
|
const card = issueCard(s.loyalty);
|
|
card.pendingRewards.push({ kind: 'free_trade' });
|
|
stock(s, 'slot-0', tetrisNew, 1);
|
|
const result = trade(s, catalog, makeCustomer({ intent: 'trade', offered: smlComplete, cardId: card.id }), 'slot-0', 1139);
|
|
expect(result.customerPaysCents).toBe(0);
|
|
expect(card.pendingRewards).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('requests', () => {
|
|
it('costs reputation when the game is missing, clamped at 0', () => {
|
|
const s = createNewGame(1);
|
|
recordLostRequest(s);
|
|
expect(s.reputation).toBe(19);
|
|
expect(s.day.lostRequests).toBe(1);
|
|
s.reputation = 0.5;
|
|
recordLostRequest(s);
|
|
expect(s.reputation).toBe(0);
|
|
});
|
|
|
|
it('earns reputation when the game is found', () => {
|
|
const s = createNewGame(1);
|
|
recordSatisfiedRequest(s);
|
|
expect(s.reputation).toBe(20.5);
|
|
});
|
|
});
|
|
|
|
describe('reward and cash edge cases', () => {
|
|
it('keeps the free trade reward when there is no difference to waive', () => {
|
|
const s = createNewGame(1);
|
|
const card = issueCard(s.loyalty);
|
|
card.pendingRewards.push({ kind: 'free_trade' });
|
|
stock(s, 'slot-0', tetrisNew, 1);
|
|
trade(s, catalog, makeCustomer({ intent: 'trade', offered: smlComplete, cardId: card.id }), 'slot-0', 3034);
|
|
expect(card.pendingRewards).toEqual([{ kind: 'free_trade' }]);
|
|
});
|
|
|
|
it('prefers a free used game over a discount', () => {
|
|
const s = createNewGame(1);
|
|
const card = issueCard(s.loyalty);
|
|
card.pendingRewards.push({ kind: 'discount_percent', percent: 10 }, { kind: 'free_used_game', maxValueCents: 2000 });
|
|
stock(s, 'slot-0', tetrisLoose, 1);
|
|
const result = sell(s, catalog, 'slot-0', makeCustomer({ cardId: card.id }));
|
|
expect(result.priceCents).toBe(0);
|
|
expect(card.pendingRewards).toEqual([{ kind: 'discount_percent', percent: 10 }]);
|
|
});
|
|
|
|
it('throws before mutating when the shop cannot pay the trade excess', () => {
|
|
const s = createNewGame(1);
|
|
stock(s, 'slot-0', tetrisNew, 1);
|
|
s.cash = 100;
|
|
expect(() => trade(s, catalog, makeCustomer({ intent: 'trade', offered: smlComplete }), 'slot-0', 4000)).toThrow('Not enough cash');
|
|
expect(getSlot(s.inventory, 'slot-0').qty).toBe(1);
|
|
expect(s.cash).toBe(100);
|
|
expect(s.day.trades).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('changeLoyaltyProgram', () => {
|
|
it('lowers reputation by 2 when the program becomes less generous', () => {
|
|
const s = createNewGame(1);
|
|
s.reputation = 50;
|
|
changeLoyaltyProgram(s, [{ stampType: 'buy_new', slots: 20, reward: { kind: 'voucher', valueCents: 500 } }]);
|
|
expect(s.reputation).toBe(48);
|
|
expect(s.loyalty.programs).toHaveLength(2);
|
|
});
|
|
|
|
it('leaves reputation unchanged when the program is more generous', () => {
|
|
const s = createNewGame(1);
|
|
s.reputation = 50;
|
|
changeLoyaltyProgram(s, [{ stampType: 'buy_new', slots: 5, reward: { kind: 'voucher', valueCents: 1000 } }]);
|
|
expect(s.reputation).toBe(50);
|
|
});
|
|
});
|