// Ported from apps/server/test (CR-014 Step 14.5): now runs on the engine. import { content } from "@deadhead/content"; import { companyResponseSchema, errorResponseSchema } from "@deadhead/schema"; import { describe, expect, it } from "vitest"; import { useTestApp, call, getState, edit, setCredits, testContract, insertDispatch, signUpOwner, type Guest, } from "../support"; const testApp = useTestApp(); async function setLoan(_guest: Guest, loanBalance: number) { await edit(testApp().app, (save) => { save.company.loanBalance = loanBalance; }); } const share = content.config.loanRepaymentShare; describe("loan repayment on delivery", () => { it("adds the net to credits, lowers the balance by the repayment, and records it", async () => { const { app, clock } = testApp(); const owner = await signUpOwner(app); await setCredits(testApp().app, 1000); await setLoan(owner, 5000); const record = await insertDispatch(testApp().app, { shipId: owner.shipId, pilotId: owner.avatarId, departAt: clock.now, contract: testContract({ payout: 2000 }), }); clock.now = record.arrivalAt; const state = await getState(app, owner); // The avatar flew it, so there is no pilot pay. const repayment = Math.floor(2000 * share); expect(state.company).toMatchObject({ credits: 1000 + 2000 - repayment, loanBalance: 5000 - repayment, }); expect(state.summaries[0]).toMatchObject({ deadhead: false, credits: 2000 - repayment, loanRepayment: repayment, }); }); it("repays only what is owed when the balance is small", async () => { const { app, clock } = testApp(); const owner = await signUpOwner(app); await setCredits(testApp().app, 0); await setLoan(owner, 50); const record = await insertDispatch(testApp().app, { shipId: owner.shipId, pilotId: owner.avatarId, departAt: clock.now, contract: testContract({ payout: 2000 }), }); clock.now = record.arrivalAt; const state = await getState(app, owner); expect(state.company).toMatchObject({ credits: 1950, loanBalance: 0 }); }); it("puts the full amount into credits when there is no balance", async () => { const { app, clock } = testApp(); const owner = await signUpOwner(app); await setCredits(testApp().app, 0); const record = await insertDispatch(testApp().app, { shipId: owner.shipId, pilotId: owner.avatarId, departAt: clock.now, contract: testContract({ payout: 2000 }), }); clock.now = record.arrivalAt; const state = await getState(app, owner); expect(state.company).toMatchObject({ credits: 2000, loanBalance: 0 }); expect(state.summaries[0]!.loanRepayment).toBe(0); }); }); describe("paying down the loan", () => { const repay = (guest: Guest, amount: number) => call(testApp().app, guest, "POST", "/api/loan/repay", { amount }); it("moves the amount from credits to the loan", async () => { const owner = await signUpOwner(testApp().app); await setCredits(testApp().app, 3000); await setLoan(owner, 5000); const response = await repay(owner, 1200); expect(response.statusCode).toBe(200); expect(companyResponseSchema.parse(response.json()).company).toMatchObject({ credits: 1800, loanBalance: 3800, }); }); it("takes only the balance when asked to repay more", async () => { const owner = await signUpOwner(testApp().app); await setCredits(testApp().app, 3000); await setLoan(owner, 400); const { company } = companyResponseSchema.parse((await repay(owner, 2500)).json()); expect(company).toMatchObject({ credits: 2600, loanBalance: 0 }); }); it("fails with no changes when repaying more than the player has", async () => { const { app } = testApp(); const owner = await signUpOwner(app); await setCredits(testApp().app, 300); await setLoan(owner, 5000); const response = await repay(owner, 1000); expect(response.statusCode).toBe(409); expect(errorResponseSchema.parse(response.json()).error.code).toBe("insufficient-funds"); expect((await getState(app, owner)).company).toMatchObject({ credits: 300, loanBalance: 5000 }); }); it("rejects a non-positive amount", async () => { const owner = await signUpOwner(testApp().app); expect((await repay(owner, 0)).statusCode).toBe(400); }); });