[{"data":1,"prerenderedAt":151},["ShallowReactive",2],{"games":3,"levels:legacy-nightmare":107,"level:legacy-nightmare:invoice-engine":114},[4,17,30,41,52,63,74,85,96],{"id":5,"order":6,"domain":7,"glyph":8,"requiresSandbox":9,"title":10,"tagline":13,"levelCount":6,"status":16},"legacy-nightmare",1,"refactoring","{ }",true,{"fr":11,"en":12},"Le cauchemar du Legacy","Legacy Nightmare",{"fr":14,"en":15},"Corrige le bug caché dans un code spaghetti, sans rien casser.","Fix the bug buried in spaghetti code without breaking anything.","available",{"id":18,"order":19,"domain":20,"glyph":20,"requiresSandbox":21,"title":22,"tagline":25,"levelCount":28,"status":29},"git-untangle",2,"git",false,{"fr":23,"en":24},"Démêlage d'historique","History Untangling",{"fr":26,"en":27},"Transforme un graphe de commits chaotique en historique propre.","Turn a chaotic commit graph into a clean history.",0,"coming-soon",{"id":31,"order":32,"domain":33,"glyph":34,"requiresSandbox":21,"title":35,"tagline":38,"levelCount":28,"status":29},"prod-pressure",3,"devops","RPS",{"fr":36,"en":37},"Pression en Prod","Prod Pressure",{"fr":39,"en":40},"Fais tenir la prod pendant le Black Friday, sans exploser le budget.","Keep production alive through Black Friday, on budget.",{"id":42,"order":43,"domain":44,"glyph":45,"requiresSandbox":9,"title":46,"tagline":49,"levelCount":28,"status":29},"the-heist",4,"security","0day",{"fr":47,"en":48},"Le braquage","The Heist",{"fr":50,"en":51},"Lis l'API, trouve la faille logique, exploite-la.","Read the API, find the logic flaw, exploit it.",{"id":53,"order":54,"domain":55,"glyph":56,"requiresSandbox":21,"title":57,"tagline":60,"levelCount":6,"status":16},"tech-dilemmas",5,"product","A|B",{"fr":58,"en":59},"Dilemmes","Tech Dilemmas",{"fr":61,"en":62},"Survis à un sprint d'arbitrages impossibles.","Survive a sprint of impossible trade-offs.",{"id":64,"order":65,"domain":66,"glyph":67,"requiresSandbox":21,"title":68,"tagline":71,"levelCount":28,"status":29},"cap-workshop",6,"distributed","CAP",{"fr":69,"en":70},"L'Atelier du Théorème CAP","CAP Workshop",{"fr":72,"en":73},"Choisis les bons compromis avant que le réseau ne se coupe.","Pick the right trade-offs before the network splits.",{"id":75,"order":76,"domain":77,"glyph":78,"requiresSandbox":9,"title":79,"tagline":82,"levelCount":28,"status":29},"feature-creeps",7,"patterns","\u003CT>",{"fr":80,"en":81},"L'attaque des Feature Creeps","Attack of the Feature Creeps",{"fr":83,"en":84},"Absorbe des règles métier sans fin sans réécrire ton cœur.","Absorb endless business rules without rewriting your core.",{"id":86,"order":87,"domain":88,"glyph":89,"requiresSandbox":21,"title":90,"tagline":93,"levelCount":32,"status":16},"poisoned-review",8,"review","PR",{"fr":91,"en":92},"La Code Review Empoisonnée","Poisoned Code Review",{"fr":94,"en":95},"Une PR parfaite en apparence cache une bombe. Trouve la ligne.","A flawless-looking PR hides a time bomb. Find the line.",{"id":97,"order":98,"domain":99,"glyph":100,"requiresSandbox":21,"hidden":9,"title":101,"tagline":104,"levelCount":6,"status":16},"engine-demo",99,"demo","?",{"fr":102,"en":103},"Démo du moteur de niveaux","Level engine demo",{"fr":105,"en":106},"Niveau factice qui exerce le moteur de bout en bout : indices, validation, score, débrief.","Dummy level exercising the engine end to end: hints, validation, score, debrief.",[108],{"id":109,"gameId":5,"order":6,"title":110,"difficulty":32,"estimatedMinutes":113},"invoice-engine",{"fr":111,"en":112},"Le moteur de facturation","The invoicing engine",25,{"id":109,"gameId":5,"order":6,"title":115,"difficulty":32,"estimatedMinutes":113,"context":116,"objective":119,"tools":122,"initialState":127,"hints":141,"scoring":148},{"fr":111,"en":112},{"fr":117,"en":118},"Ticket **SUP-2291**, priorité haute : *« Le calcul de TVA plante pour les clients internationaux. »* La commande d'un client suisse a fait tomber le checkout ce matin.\n\nLe suspect : `computeInvoice`, écrite en 2019 par Gérard, qui a quitté l'entreprise depuis. Personne n'ose y toucher, mais elle est couverte par une suite de tests de caractérisation (`tests\u002Finvoice.test.ts`). Le support a ajouté `tests\u002Fexport.test.ts`, qui reproduit le ticket. Il est rouge.","Ticket **SUP-2291**, high priority: *“VAT computation crashes for international customers.”* An order from a Swiss customer took the checkout down this morning.\n\nThe suspect: `computeInvoice`, written in 2019 by Gérard, who has since left the company. Nobody dares to touch it, but it is covered by a characterization test suite (`tests\u002Finvoice.test.ts`). Support added `tests\u002Fexport.test.ts`, which reproduces the ticket. It is red.",{"fr":120,"en":121},"1. Fais passer `tests\u002Fexport.test.ts` au vert : hors de l'UE, une facture est émise **sans TVA** (régime `export`).\n2. Ne casse aucun test de `tests\u002Finvoice.test.ts`.\n3. Laisse le code plus lisible que tu ne l'as trouvé : la complexité cyclomatique de `computeInvoice` doit tomber à **8 ou moins**. La fonction garde son nom et reste dans `src\u002Finvoice.ts`.\n\n`⌘S` \u002F `Ctrl+S` lance les tests. Soumets quand tout est vert : chaque soumission ratée coûte des points.","1. Turn `tests\u002Fexport.test.ts` green: outside the EU, an invoice is issued **without VAT** (`export` mode).\n2. Do not break any test of `tests\u002Finvoice.test.ts`.\n3. Leave the code more readable than you found it: the cyclomatic complexity of `computeInvoice` must go down to **8 or less**. The function keeps its name and stays in `src\u002Finvoice.ts`.\n\n`⌘S` \u002F `Ctrl+S` runs the tests. Submit once everything is green: each failed submission costs points.",[123,124,125,126],"code-editor","test-runner","terminal","complexity",{"files":128,"testFiles":135,"openFile":138,"target":139},{"src\u002Finvoice.ts":129,"src\u002Frates.ts":130,"src\u002Fmoney.ts":131,"src\u002Ftypes.ts":132,"tests\u002Finvoice.test.ts":133,"tests\u002Fexport.test.ts":134},"import { EU, TX } from '.\u002Frates'\nimport { pct, r2 } from '.\u002Fmoney'\nimport type { Invoice, Order } from '.\u002Ftypes'\n\n\u002F\u002F Invoice computation. DO NOT TOUCH without asking Gérard\n\u002F\u002F (Gérard left in 2021)\nexport function computeInvoice(o: Order): Invoice {\n  const c = o.customer\n  const res: any = { mode: 'domestic', lines: [], net: 0, vat: 0, total: 0 }\n  const t: any = TX[c.country]\n  let flg = false\n  if (c.country !== 'FR') {\n    if (c.kind === 'B2B' && c.vatNumber && c.vatNumber.length > 4 && c.vatNumber.slice(0, 2) === c.country) {\n      flg = true\n      res.mode = 'reverse-charge'\n    }\n    else {\n      res.mode = 'oss'\n    }\n  }\n  for (let i = 0; i \u003C o.lines.length; i++) {\n    const l = o.lines[i]\n    let p = l.unitPrice * l.qty\n    if (l.qty >= 10) p = p * 0.95\n    if (o.coupon) {\n      if (o.coupon === 'WELCOME10') {\n        p = p * 0.9\n      }\n      else if (o.coupon === 'VIP' && c.vip) {\n        p = p * 0.8\n      }\n    }\n    p = r2(p)\n    let rt = 0\n    if (!flg) {\n      if (l.category === 'book' || l.category === 'food') {\n        rt = c.country === 'FR' ? TX.FR.red : t.red\n      }\n      else if (l.category === 'digital') {\n        rt = c.country === 'FR' ? TX.FR.std : t.std\n      }\n      else {\n        rt = c.country === 'FR' ? TX.FR.std : t.std\n      }\n    }\n    const v = r2(pct(p, rt))\n    res.lines.push({ sku: l.sku, net: p, rate: rt, vat: v })\n    res.net = res.net + p\n    res.vat = res.vat + v\n  }\n  \u002F\u002F shipping\n  if (o.shipping > 0) {\n    let s = o.shipping\n    if (c.country === 'FR' && res.net >= 100) s = 0 \u002F\u002F free shipping (France only)\n    if (s > 0) {\n      let rt = 0\n      if (!flg) rt = c.country === 'FR' ? TX.FR.std : t.std\n      const v = r2(pct(s, rt))\n      res.lines.push({ sku: 'SHIPPING', net: s, rate: rt, vat: v })\n      res.net = res.net + s\n      res.vat = res.vat + v\n    }\n  }\n  res.net = r2(res.net)\n  res.vat = r2(res.vat)\n  res.total = r2(res.net + res.vat)\n  return res\n}\n","\u002F\u002F VAT rates in percent (std = standard, red = reduced). Simplified: one reduced rate per country.\n\u002F\u002F Last update: 2019. TODO check Brexit impact\nexport const TX: Record\u003Cstring, { std: number, red: number }> = {\n  AT: { std: 20, red: 10 },\n  BE: { std: 21, red: 6 },\n  BG: { std: 20, red: 9 },\n  CY: { std: 19, red: 5 },\n  CZ: { std: 21, red: 12 },\n  DE: { std: 19, red: 7 },\n  DK: { std: 25, red: 25 },\n  EE: { std: 24, red: 9 },\n  ES: { std: 21, red: 10 },\n  FI: { std: 25.5, red: 10 },\n  FR: { std: 20, red: 5.5 },\n  GB: { std: 20, red: 5 },\n  GR: { std: 24, red: 6 },\n  HR: { std: 25, red: 5 },\n  HU: { std: 27, red: 5 },\n  IE: { std: 23, red: 13.5 },\n  IT: { std: 22, red: 10 },\n  LT: { std: 21, red: 9 },\n  LU: { std: 17, red: 8 },\n  LV: { std: 21, red: 12 },\n  MT: { std: 18, red: 5 },\n  NL: { std: 21, red: 9 },\n  PL: { std: 23, red: 8 },\n  PT: { std: 23, red: 6 },\n  RO: { std: 21, red: 11 },\n  SE: { std: 25, red: 6 },\n  SI: { std: 22, red: 9.5 },\n  SK: { std: 23, red: 19 },\n}\n\n\u002F\u002F EU member states\nexport const EU = [\n  'AT', 'BE', 'BG', 'CY', 'CZ', 'DE', 'DK', 'EE', 'ES', 'FI', 'FR', 'GR', 'HR', 'HU',\n  'IE', 'IT', 'LT', 'LU', 'LV', 'MT', 'NL', 'PL', 'PT', 'RO', 'SE', 'SI', 'SK',\n]\n","\u002F\u002F Rounds a positive amount to the cent\nexport function r2(x: number): number {\n  return Math.round((x + Number.EPSILON) * 100) \u002F 100\n}\n\nexport function pct(x: number, p: number): number {\n  return (x * p) \u002F 100\n}\n","export type Category = 'standard' | 'book' | 'food' | 'digital'\n\nexport interface Customer {\n  id: string\n  \u002F** ISO 3166-1 alpha-2 country code *\u002F\n  country: string\n  kind: 'B2B' | 'B2C'\n  vatNumber?: string\n  vip?: boolean\n}\n\nexport interface OrderLine {\n  sku: string\n  qty: number\n  unitPrice: number\n  category: Category\n}\n\nexport interface Order {\n  customer: Customer\n  lines: OrderLine[]\n  coupon?: string\n  \u002F** Shipping fee, excluding VAT *\u002F\n  shipping: number\n}\n\nexport type VatMode = 'domestic' | 'oss' | 'reverse-charge' | 'export'\n\nexport interface InvoiceLine {\n  sku: string\n  net: number\n  \u002F** Percent *\u002F\n  rate: number\n  vat: number\n}\n\nexport interface Invoice {\n  mode: VatMode\n  lines: InvoiceLine[]\n  net: number\n  vat: number\n  total: number\n}\n","import { describe, expect, it } from 'vitest'\nimport { computeInvoice } from '..\u002Fsrc\u002Finvoice'\nimport type { Customer, OrderLine } from '..\u002Fsrc\u002Ftypes'\n\nconst consumer = (country: string): Customer => ({ id: 'c-1', country, kind: 'B2C' })\nconst company = (country: string, vatNumber?: string): Customer => ({ id: 'c-2', country, kind: 'B2B', vatNumber })\nconst line = (qty: number, unitPrice: number, category: OrderLine['category'] = 'standard'): OrderLine =>\n  ({ sku: `SKU-${category}-${unitPrice}`, qty, unitPrice, category })\n\ndescribe('domestic sales', () => {\n  it('applies the standard rate', () => {\n    const invoice = computeInvoice({ customer: consumer('FR'), lines: [line(2, 50)], shipping: 0 })\n    expect(invoice).toMatchObject({ mode: 'domestic', net: 100, vat: 20, total: 120 })\n  })\n\n  it('applies the reduced rate to books and food', () => {\n    const invoice = computeInvoice({ customer: consumer('FR'), lines: [line(1, 30, 'book'), line(2, 4, 'food')], shipping: 0 })\n    expect(invoice.lines.map(item => item.rate)).toEqual([5.5, 5.5])\n    expect(invoice).toMatchObject({ net: 38, vat: 2.09, total: 40.09 })\n  })\n\n  it('treats a French company like a consumer', () => {\n    const invoice = computeInvoice({ customer: company('FR', 'FR40123456789'), lines: [line(1, 200)], shipping: 0 })\n    expect(invoice).toMatchObject({ mode: 'domestic', vat: 40, total: 240 })\n  })\n\n  it('charges shipping at the standard rate', () => {\n    const invoice = computeInvoice({ customer: consumer('FR'), lines: [line(1, 40)], shipping: 9.9 })\n    expect(invoice.lines.at(-1)).toEqual({ sku: 'SHIPPING', net: 9.9, rate: 20, vat: 1.98 })\n    expect(invoice).toMatchObject({ net: 49.9, vat: 9.98, total: 59.88 })\n  })\n\n  it('offers shipping from 100 € of goods', () => {\n    const invoice = computeInvoice({ customer: consumer('FR'), lines: [line(2, 50)], shipping: 9.9 })\n    expect(invoice.lines).toHaveLength(1)\n    expect(invoice.total).toBe(120)\n  })\n})\n\ndescribe('discounts', () => {\n  it('takes 5 % off from 10 units', () => {\n    expect(computeInvoice({ customer: consumer('FR'), lines: [line(10, 10)], shipping: 0 }).net).toBe(95)\n  })\n\n  it('applies the WELCOME10 coupon', () => {\n    expect(computeInvoice({ customer: consumer('FR'), lines: [line(1, 100)], coupon: 'WELCOME10', shipping: 0 }).net).toBe(90)\n  })\n\n  it('applies the VIP coupon to VIP customers only', () => {\n    const order = { lines: [line(1, 100)], coupon: 'VIP', shipping: 0 }\n    expect(computeInvoice({ ...order, customer: { ...consumer('FR'), vip: true } }).net).toBe(80)\n    expect(computeInvoice({ ...order, customer: consumer('FR') }).net).toBe(100)\n  })\n\n  it('stacks the bulk discount and the coupon', () => {\n    expect(computeInvoice({ customer: consumer('FR'), lines: [line(10, 10)], coupon: 'WELCOME10', shipping: 0 }).net).toBe(85.5)\n  })\n})\n\ndescribe('sales to the EU', () => {\n  it('charges consumers the VAT of their country', () => {\n    const invoice = computeInvoice({ customer: consumer('DE'), lines: [line(1, 100), line(1, 20, 'book')], shipping: 0 })\n    expect(invoice.lines.map(item => item.rate)).toEqual([19, 7])\n    expect(invoice).toMatchObject({ mode: 'oss', vat: 20.4, total: 140.4 })\n  })\n\n  it('reverse-charges companies with a valid VAT number', () => {\n    const invoice = computeInvoice({ customer: company('DE', 'DE123456789'), lines: [line(1, 100)], shipping: 10 })\n    expect(invoice).toMatchObject({ mode: 'reverse-charge', net: 110, vat: 0, total: 110 })\n  })\n\n  it('charges VAT to companies whose VAT number is from another country', () => {\n    const invoice = computeInvoice({ customer: company('BE', 'FR40123456789'), lines: [line(1, 100)], shipping: 0 })\n    expect(invoice).toMatchObject({ mode: 'oss', vat: 21 })\n  })\n\n  it('charges shipping at the destination rate, without free shipping', () => {\n    const invoice = computeInvoice({ customer: consumer('ES'), lines: [line(2, 60)], shipping: 12 })\n    expect(invoice).toMatchObject({ net: 132, vat: 27.72, total: 159.72 })\n  })\n})\n\ndescribe('rounding', () => {\n  it('rounds each line to the cent', () => {\n    const invoice = computeInvoice({ customer: consumer('FR'), lines: [line(3, 3.33, 'book'), line(1, 0.99)], shipping: 0 })\n    expect(invoice.lines.map(item => item.vat)).toEqual([0.55, 0.2])\n    expect(invoice).toMatchObject({ net: 10.98, vat: 0.75, total: 11.73 })\n  })\n})\n","import { describe, expect, it } from 'vitest'\nimport { computeInvoice } from '..\u002Fsrc\u002Finvoice'\nimport type { Customer, Order } from '..\u002Fsrc\u002Ftypes'\n\n\u002F\u002F SUP-2291 \"VAT computation crashes for international customers\"\nconst order = (customer: Customer): Order => ({\n  customer,\n  lines: [\n    { sku: 'KB-01', qty: 2, unitPrice: 50, category: 'standard' },\n    { sku: 'BK-07', qty: 1, unitPrice: 30, category: 'book' },\n  ],\n  shipping: 25,\n})\n\ndescribe('exports outside the EU', () => {\n  it.each([\n    { country: 'CH', kind: 'B2C' },\n    { country: 'US', kind: 'B2B' },\n    { country: 'GB', kind: 'B2C' },\n  ] as const)('invoices a $kind customer from $country without VAT', ({ country, kind }) => {\n    const invoice = computeInvoice(order({ id: 'c-9', country, kind }))\n    expect(invoice.mode).toBe('export')\n    expect(invoice).toMatchObject({ net: 155, vat: 0, total: 155 })\n  })\n\n  it('ignores the tax id of a foreign company', () => {\n    const invoice = computeInvoice(order({ id: 'c-9', country: 'US', kind: 'B2B', vatNumber: 'US98-7654321' }))\n    expect(invoice.mode).toBe('export')\n    expect(invoice.vat).toBe(0)\n  })\n})\n",[136,137],"tests\u002Finvoice.test.ts","tests\u002Fexport.test.ts","src\u002Finvoice.ts",{"path":138,"name":140,"baseline":113},"computeInvoice",[142,144,146],{"type":143},"observation",{"type":145},"reflection",{"type":147},"focus",{"base":149,"parTimeSeconds":150},150,1500,1790619203713]