@bisondesk/core-sdk 1.0.676 → 1.0.678

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/lib/apis/organization-bank-accounts.d.ts +11 -0
  2. package/lib/apis/organization-bank-accounts.d.ts.map +1 -0
  3. package/lib/apis/organization-bank-accounts.js +47 -0
  4. package/lib/apis/organization-bank-accounts.js.map +1 -0
  5. package/lib/types/opportunities.d.ts +1 -0
  6. package/lib/types/opportunities.d.ts.map +1 -1
  7. package/lib/types/opportunities.js.map +1 -1
  8. package/lib/types/organization-bank-accounts.d.ts +35 -0
  9. package/lib/types/organization-bank-accounts.d.ts.map +1 -0
  10. package/lib/types/organization-bank-accounts.js +9 -0
  11. package/lib/types/organization-bank-accounts.js.map +1 -0
  12. package/lib/types/payment-exports.d.ts +23 -0
  13. package/lib/types/payment-exports.d.ts.map +1 -0
  14. package/lib/types/payment-exports.js +2 -0
  15. package/lib/types/payment-exports.js.map +1 -0
  16. package/lib/types/quote-settings.d.ts +1 -0
  17. package/lib/types/quote-settings.d.ts.map +1 -1
  18. package/lib/types/quote-settings.js.map +1 -1
  19. package/lib/types/quotes.d.ts +1 -0
  20. package/lib/types/quotes.d.ts.map +1 -1
  21. package/lib/types/quotes.js.map +1 -1
  22. package/lib/types/tenants.d.ts +1 -0
  23. package/lib/types/tenants.d.ts.map +1 -1
  24. package/lib/types/tenants.js +1 -0
  25. package/lib/types/tenants.js.map +1 -1
  26. package/lib/types/vehicle-purchase-payments.d.ts +51 -0
  27. package/lib/types/vehicle-purchase-payments.d.ts.map +1 -1
  28. package/lib/types/vehicle-purchase-payments.js +6 -0
  29. package/lib/types/vehicle-purchase-payments.js.map +1 -1
  30. package/lib/types/vehicles.d.ts +1 -0
  31. package/lib/types/vehicles.d.ts.map +1 -1
  32. package/lib/types/vehicles.js.map +1 -1
  33. package/lib/utils/organization-bank-accounts.d.ts +3 -0
  34. package/lib/utils/organization-bank-accounts.d.ts.map +1 -0
  35. package/lib/utils/organization-bank-accounts.js +9 -0
  36. package/lib/utils/organization-bank-accounts.js.map +1 -0
  37. package/lib/utils/tenants.d.ts +1 -0
  38. package/lib/utils/tenants.d.ts.map +1 -1
  39. package/lib/utils/tenants.js +5 -0
  40. package/lib/utils/tenants.js.map +1 -1
  41. package/lib/utils/tenants.test.js +18 -1
  42. package/lib/utils/tenants.test.js.map +1 -1
  43. package/package.json +1 -1
  44. package/src/apis/organization-bank-accounts.ts +123 -0
  45. package/src/types/opportunities.ts +1 -0
  46. package/src/types/organization-bank-accounts.ts +56 -0
  47. package/src/types/payment-exports.ts +40 -0
  48. package/src/types/quote-settings.ts +2 -0
  49. package/src/types/quotes.ts +2 -0
  50. package/src/types/tenants.ts +7 -0
  51. package/src/types/vehicle-purchase-payments.ts +116 -0
  52. package/src/types/vehicles.ts +3 -0
  53. package/src/utils/organization-bank-accounts.ts +19 -0
  54. package/src/utils/tenants.test.ts +22 -2
  55. package/src/utils/tenants.ts +11 -0
  56. package/tsconfig.tsbuildinfo +1 -1
@@ -1,4 +1,5 @@
1
1
  import { XError } from '@bisondesk/commons-sdk/errors';
2
+ import { isEUCountry } from '@bisondesk/commons-sdk/locations';
2
3
  import { first } from 'lodash-es';
3
4
  export const getBranch = (tenant, branchId) => {
4
5
  const branch = tenant.branches.find((branch) => branch.id === branchId);
@@ -8,6 +9,10 @@ export const getBranch = (tenant, branchId) => {
8
9
  return branch;
9
10
  };
10
11
  export const getDefaultBranch = (tenant) => first(tenant.branches);
12
+ export const getBranchAskingVies = (tenant, branchId) => {
13
+ const europeanBranches = tenant.branches.filter((branch) => isEUCountry(branch.country));
14
+ return europeanBranches.find((branch) => branch.id === branchId) ?? europeanBranches.at(0);
15
+ };
11
16
  export const isDropshippingBranch = (branch) => branch.dropshipping === true;
12
17
  export const getBranchStandardVatRate = (tenant, branchId) => {
13
18
  const branch = getBranch(tenant, branchId);
@@ -1 +1 @@
1
- {"version":3,"file":"tenants.js","sourceRoot":"/","sources":["utils/tenants.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,+BAA+B,CAAC;AACvD,OAAO,EAAE,KAAK,EAAE,MAAM,WAAW,CAAC;AAGlC,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,MAAc,EAAE,QAAgB,EAAU,EAAE;IACpE,MAAM,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,MAAc,EAAE,EAAE,CAAC,MAAM,CAAC,EAAE,KAAK,QAAQ,CAAC,CAAC;IAChF,IAAI,MAAM,IAAI,IAAI,EAAE,CAAC;QACnB,MAAM,IAAI,MAAM,CAAC,0BAA0B,EAAE,EAAE,QAAQ,EAAE,MAAM,CAAC,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAC;IAClF,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,MAAc,EAAU,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,QAAQ,CAAE,CAAC;AAIpF,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,MAAc,EAAW,EAAE,CAAC,MAAM,CAAC,YAAY,KAAK,IAAI,CAAC;AAE9F,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC,MAAc,EAAE,QAAgB,EAAU,EAAE;IACnF,MAAM,MAAM,GAAW,SAAS,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IACnD,OAAO,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC;AAClC,CAAC,CAAC;AAOF,MAAM,CAAC,MAAM,0BAA0B,GAAG,CAAC,CAAS,EAAE,CAAS,EAAW,EAAE,CAC1E,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,OAAO;IACvB,CAAC,CAAC,QAAQ,KAAK,CAAC,CAAC,QAAQ;IACzB,CAAC,CAAC,QAAQ,CAAC,QAAQ,KAAK,CAAC,CAAC,QAAQ,CAAC,QAAQ;IAC3C,CAAC,CAAC,CAAC,aAAa,IAAI,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,aAAa,IAAI,KAAK,CAAC,CAAC","sourcesContent":["import { XError } from '@bisondesk/commons-sdk/errors';\nimport { first } from 'lodash-es';\nimport { Branch, Tenant } from '../types/tenants.js';\n\nexport const getBranch = (tenant: Tenant, branchId: string): Branch => {\n const branch = tenant.branches.find((branch: Branch) => branch.id === branchId);\n if (branch == null) {\n throw new XError('tenants.branch-not-found', { tenantId: tenant.id, branchId });\n }\n\n return branch;\n};\n\nexport const getDefaultBranch = (tenant: Tenant): Branch => first(tenant.branches)!;\n\n/** The source invoices the end customer directly; this branch collects payment and earns\n * commission (see {@link Branch.dropshipping}). */\nexport const isDropshippingBranch = (branch: Branch): boolean => branch.dropshipping === true;\n\nexport const getBranchStandardVatRate = (tenant: Tenant, branchId: string): string => {\n const branch: Branch = getBranch(tenant, branchId);\n return branch.vatRates.standard;\n};\n\n/**\n * Whether a quote computed for one branch is still right for the other. A quote's VAT regime,\n * currency, calculator strategy and conditions all come from these four fields, so when they\n * agree the quote can move with its deal; otherwise the deal needs a new one.\n */\nexport const areBranchesQuoteCompatible = (a: Branch, b: Branch): boolean =>\n a.country === b.country &&\n a.currency === b.currency &&\n a.vatRates.standard === b.vatRates.standard &&\n (a.externalQuote ?? false) === (b.externalQuote ?? false);\n"]}
1
+ {"version":3,"file":"tenants.js","sourceRoot":"/","sources":["utils/tenants.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,+BAA+B,CAAC;AACvD,OAAO,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAC;AAC/D,OAAO,EAAE,KAAK,EAAE,MAAM,WAAW,CAAC;AAGlC,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,MAAc,EAAE,QAAgB,EAAU,EAAE;IACpE,MAAM,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,MAAc,EAAE,EAAE,CAAC,MAAM,CAAC,EAAE,KAAK,QAAQ,CAAC,CAAC;IAChF,IAAI,MAAM,IAAI,IAAI,EAAE,CAAC;QACnB,MAAM,IAAI,MAAM,CAAC,0BAA0B,EAAE,EAAE,QAAQ,EAAE,MAAM,CAAC,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAC;IAClF,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,MAAc,EAAU,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,QAAQ,CAAE,CAAC;AAOpF,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,MAAc,EAAE,QAAiB,EAAsB,EAAE;IAC3F,MAAM,gBAAgB,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;IACzF,OAAO,gBAAgB,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,EAAE,KAAK,QAAQ,CAAC,IAAI,gBAAgB,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;AAC7F,CAAC,CAAC;AAIF,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,MAAc,EAAW,EAAE,CAAC,MAAM,CAAC,YAAY,KAAK,IAAI,CAAC;AAE9F,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC,MAAc,EAAE,QAAgB,EAAU,EAAE;IACnF,MAAM,MAAM,GAAW,SAAS,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IACnD,OAAO,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC;AAClC,CAAC,CAAC;AAOF,MAAM,CAAC,MAAM,0BAA0B,GAAG,CAAC,CAAS,EAAE,CAAS,EAAW,EAAE,CAC1E,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,OAAO;IACvB,CAAC,CAAC,QAAQ,KAAK,CAAC,CAAC,QAAQ;IACzB,CAAC,CAAC,QAAQ,CAAC,QAAQ,KAAK,CAAC,CAAC,QAAQ,CAAC,QAAQ;IAC3C,CAAC,CAAC,CAAC,aAAa,IAAI,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,aAAa,IAAI,KAAK,CAAC,CAAC","sourcesContent":["import { XError } from '@bisondesk/commons-sdk/errors';\nimport { isEUCountry } from '@bisondesk/commons-sdk/locations';\nimport { first } from 'lodash-es';\nimport { Branch, Tenant } from '../types/tenants.js';\n\nexport const getBranch = (tenant: Tenant, branchId: string): Branch => {\n const branch = tenant.branches.find((branch: Branch) => branch.id === branchId);\n if (branch == null) {\n throw new XError('tenants.branch-not-found', { tenantId: tenant.id, branchId });\n }\n\n return branch;\n};\n\nexport const getDefaultBranch = (tenant: Tenant): Branch => first(tenant.branches)!;\n\n/**\n * The branch whose VAT number VIES records as the requester of a VAT check, which earns a\n * consultation number: the branch the check is made for when it is European, else the tenant's\n * first European branch. A tenant without a European branch asks VIES anonymously.\n */\nexport const getBranchAskingVies = (tenant: Tenant, branchId?: string): Branch | undefined => {\n const europeanBranches = tenant.branches.filter((branch) => isEUCountry(branch.country));\n return europeanBranches.find((branch) => branch.id === branchId) ?? europeanBranches.at(0);\n};\n\n/** The source invoices the end customer directly; this branch collects payment and earns\n * commission (see {@link Branch.dropshipping}). */\nexport const isDropshippingBranch = (branch: Branch): boolean => branch.dropshipping === true;\n\nexport const getBranchStandardVatRate = (tenant: Tenant, branchId: string): string => {\n const branch: Branch = getBranch(tenant, branchId);\n return branch.vatRates.standard;\n};\n\n/**\n * Whether a quote computed for one branch is still right for the other. A quote's VAT regime,\n * currency, calculator strategy and conditions all come from these four fields, so when they\n * agree the quote can move with its deal; otherwise the deal needs a new one.\n */\nexport const areBranchesQuoteCompatible = (a: Branch, b: Branch): boolean =>\n a.country === b.country &&\n a.currency === b.currency &&\n a.vatRates.standard === b.vatRates.standard &&\n (a.externalQuote ?? false) === (b.externalQuote ?? false);\n"]}
@@ -1,4 +1,4 @@
1
- import { areBranchesQuoteCompatible } from './tenants.js';
1
+ import { areBranchesQuoteCompatible, getBranchAskingVies } from './tenants.js';
2
2
  const createBranch = (override) => ({
3
3
  id: 'TTC',
4
4
  name: 'TTC Belgium',
@@ -36,4 +36,21 @@ describe('areBranchesQuoteCompatible', () => {
36
36
  expect(areBranchesQuoteCompatible(implicit, explicit)).toBe(true);
37
37
  });
38
38
  });
39
+ describe('getBranchAskingVies', () => {
40
+ const bolivia = createBranch({ id: 'BO', country: 'bo', vatNumber: '387032026' });
41
+ const belgium = createBranch({ id: 'BE', country: 'be', vatNumber: 'BE0405567886' });
42
+ const france = createBranch({ id: 'FR', country: 'fr', vatNumber: 'FR10821739638' });
43
+ const tenantOf = (branches) => ({ branches });
44
+ test.each([
45
+ ['the European branch the check is made for', 'FR', 'FR'],
46
+ ['the first European branch when no branch is given', undefined, 'BE'],
47
+ ['the first European branch when the given branch is outside the EU', 'BO', 'BE'],
48
+ ['the first European branch when the given branch is unknown', 'GONE', 'BE'],
49
+ ])('picks %s', (_label, branchId, expected) => {
50
+ expect(getBranchAskingVies(tenantOf([bolivia, belgium, france]), branchId)?.id).toBe(expected);
51
+ });
52
+ test('asks anonymously for a tenant without a European branch', () => {
53
+ expect(getBranchAskingVies(tenantOf([bolivia]), 'BO')).toBeUndefined();
54
+ });
55
+ });
39
56
  //# sourceMappingURL=tenants.test.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"tenants.test.js","sourceRoot":"/","sources":["utils/tenants.test.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,0BAA0B,EAAE,MAAM,cAAc,CAAC;AAE1D,MAAM,YAAY,GAAG,CAAC,QAA0B,EAAU,EAAE,CAAC,CAAC;IAC5D,EAAE,EAAE,KAAK;IACT,IAAI,EAAE,aAAa;IACnB,OAAO,EAAE,IAAI;IACb,QAAQ,EAAE,IAAI;IACd,QAAQ,EAAE,KAAK;IACf,iBAAiB,EAAE,IAAI;IACvB,iBAAiB,EAAE,IAAI;IACvB,SAAS,EAAE,cAAc;IACzB,QAAQ,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE;IAC5B,QAAQ,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE;IAC3B,GAAG,QAAQ;CACZ,CAAC,CAAC;AAEH,QAAQ,CAAC,4BAA4B,EAAE,GAAG,EAAE;IAC1C,EAAE,CAAC,+EAA+E,EAAE,GAAG,EAAE;QACvF,MAAM,CAAC,GAAG,YAAY,EAAE,CAAC;QACzB,MAAM,CAAC,GAAG,YAAY,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,aAAa,EAAE,iBAAiB,EAAE,IAAI,EAAE,CAAC,CAAC;QAErF,MAAM,CAAC,0BAA0B,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACtD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2CAA2C,EAAE,GAAG,EAAE;QACnD,MAAM,MAAM,GAAG,YAAY,EAAE,CAAC;QAE9B,MAAM,CAAC,0BAA0B,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAChE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,IAAI,CAAC;QACN,CAAC,SAAS,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;QAC9B,CAAC,UAAU,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;QACjC,CAAC,UAAU,EAAE,EAAE,QAAQ,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,CAAC;QAC9C,CAAC,qBAAqB,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC;KACjD,CAAC,CAAC,oCAAoC,EAAE,CAAC,MAAM,EAAE,QAAQ,EAAE,EAAE;QAC5D,MAAM,CAAC,0BAA0B,CAAC,YAAY,EAAE,EAAE,YAAY,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACzF,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qEAAqE,EAAE,GAAG,EAAE;QAC7E,MAAM,QAAQ,GAAG,YAAY,EAAE,CAAC;QAChC,MAAM,QAAQ,GAAG,YAAY,CAAC,EAAE,aAAa,EAAE,KAAK,EAAE,CAAC,CAAC;QAExD,MAAM,CAAC,0BAA0B,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACpE,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["import { Branch } from '../types/tenants.js';\nimport { areBranchesQuoteCompatible } from './tenants.js';\n\nconst createBranch = (override?: Partial<Branch>): Branch => ({\n id: 'TTC',\n name: 'TTC Belgium',\n country: 'be',\n language: 'nl',\n currency: 'EUR',\n adminNumberPrefix: 'AT',\n stockNumberPrefix: 'TC',\n vatNumber: 'BE0123456789',\n vatRates: { standard: '21' },\n location: { country: 'be' },\n ...override,\n});\n\ndescribe('areBranchesQuoteCompatible', () => {\n it('accepts two branches that agree on country, currency, vat rate and quote mode', () => {\n const a = createBranch();\n const b = createBranch({ id: 'TTC2', name: 'TTC Antwerp', stockNumberPrefix: 'TA' });\n\n expect(areBranchesQuoteCompatible(a, b)).toBe(true);\n });\n\n it('is true for a branch compared with itself', () => {\n const branch = createBranch();\n\n expect(areBranchesQuoteCompatible(branch, branch)).toBe(true);\n });\n\n it.each([\n ['country', { country: 'fr' }],\n ['currency', { currency: 'USD' }],\n ['vat rate', { vatRates: { standard: '20' } }],\n ['external quote mode', { externalQuote: true }],\n ])('rejects branches that differ in %s', (_label, override) => {\n expect(areBranchesQuoteCompatible(createBranch(), createBranch(override))).toBe(false);\n });\n\n it('treats a missing externalQuote as false rather than as a difference', () => {\n const implicit = createBranch();\n const explicit = createBranch({ externalQuote: false });\n\n expect(areBranchesQuoteCompatible(implicit, explicit)).toBe(true);\n });\n});\n"]}
1
+ {"version":3,"file":"tenants.test.js","sourceRoot":"/","sources":["utils/tenants.test.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,0BAA0B,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAE/E,MAAM,YAAY,GAAG,CAAC,QAA0B,EAAU,EAAE,CAAC,CAAC;IAC5D,EAAE,EAAE,KAAK;IACT,IAAI,EAAE,aAAa;IACnB,OAAO,EAAE,IAAI;IACb,QAAQ,EAAE,IAAI;IACd,QAAQ,EAAE,KAAK;IACf,iBAAiB,EAAE,IAAI;IACvB,iBAAiB,EAAE,IAAI;IACvB,SAAS,EAAE,cAAc;IACzB,QAAQ,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE;IAC5B,QAAQ,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE;IAC3B,GAAG,QAAQ;CACZ,CAAC,CAAC;AAEH,QAAQ,CAAC,4BAA4B,EAAE,GAAG,EAAE;IAC1C,EAAE,CAAC,+EAA+E,EAAE,GAAG,EAAE;QACvF,MAAM,CAAC,GAAG,YAAY,EAAE,CAAC;QACzB,MAAM,CAAC,GAAG,YAAY,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,aAAa,EAAE,iBAAiB,EAAE,IAAI,EAAE,CAAC,CAAC;QAErF,MAAM,CAAC,0BAA0B,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACtD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2CAA2C,EAAE,GAAG,EAAE;QACnD,MAAM,MAAM,GAAG,YAAY,EAAE,CAAC;QAE9B,MAAM,CAAC,0BAA0B,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAChE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,IAAI,CAAC;QACN,CAAC,SAAS,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;QAC9B,CAAC,UAAU,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;QACjC,CAAC,UAAU,EAAE,EAAE,QAAQ,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,CAAC;QAC9C,CAAC,qBAAqB,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC;KACjD,CAAC,CAAC,oCAAoC,EAAE,CAAC,MAAM,EAAE,QAAQ,EAAE,EAAE;QAC5D,MAAM,CAAC,0BAA0B,CAAC,YAAY,EAAE,EAAE,YAAY,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACzF,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qEAAqE,EAAE,GAAG,EAAE;QAC7E,MAAM,QAAQ,GAAG,YAAY,EAAE,CAAC;QAChC,MAAM,QAAQ,GAAG,YAAY,CAAC,EAAE,aAAa,EAAE,KAAK,EAAE,CAAC,CAAC;QAExD,MAAM,CAAC,0BAA0B,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACpE,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,qBAAqB,EAAE,GAAG,EAAE;IACnC,MAAM,OAAO,GAAG,YAAY,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,CAAC,CAAC;IAClF,MAAM,OAAO,GAAG,YAAY,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,cAAc,EAAE,CAAC,CAAC;IACrF,MAAM,MAAM,GAAG,YAAY,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,eAAe,EAAE,CAAC,CAAC;IACrF,MAAM,QAAQ,GAAG,CAAC,QAAkB,EAAE,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAW,CAAC;IAElE,IAAI,CAAC,IAAI,CAAC;QACR,CAAC,2CAA2C,EAAE,IAAI,EAAE,IAAI,CAAC;QACzD,CAAC,mDAAmD,EAAE,SAAS,EAAE,IAAI,CAAC;QACtE,CAAC,mEAAmE,EAAE,IAAI,EAAE,IAAI,CAAC;QACjF,CAAC,4DAA4D,EAAE,MAAM,EAAE,IAAI,CAAC;KAC7E,CAAC,CAAC,UAAU,EAAE,CAAC,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,EAAE;QAC5C,MAAM,CAAC,mBAAmB,CAAC,QAAQ,CAAC,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC,EAAE,QAAQ,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACjG,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,yDAAyD,EAAE,GAAG,EAAE;QACnE,MAAM,CAAC,mBAAmB,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,aAAa,EAAE,CAAC;IACzE,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["import { Branch, Tenant } from '../types/tenants.js';\nimport { areBranchesQuoteCompatible, getBranchAskingVies } from './tenants.js';\n\nconst createBranch = (override?: Partial<Branch>): Branch => ({\n id: 'TTC',\n name: 'TTC Belgium',\n country: 'be',\n language: 'nl',\n currency: 'EUR',\n adminNumberPrefix: 'AT',\n stockNumberPrefix: 'TC',\n vatNumber: 'BE0123456789',\n vatRates: { standard: '21' },\n location: { country: 'be' },\n ...override,\n});\n\ndescribe('areBranchesQuoteCompatible', () => {\n it('accepts two branches that agree on country, currency, vat rate and quote mode', () => {\n const a = createBranch();\n const b = createBranch({ id: 'TTC2', name: 'TTC Antwerp', stockNumberPrefix: 'TA' });\n\n expect(areBranchesQuoteCompatible(a, b)).toBe(true);\n });\n\n it('is true for a branch compared with itself', () => {\n const branch = createBranch();\n\n expect(areBranchesQuoteCompatible(branch, branch)).toBe(true);\n });\n\n it.each([\n ['country', { country: 'fr' }],\n ['currency', { currency: 'USD' }],\n ['vat rate', { vatRates: { standard: '20' } }],\n ['external quote mode', { externalQuote: true }],\n ])('rejects branches that differ in %s', (_label, override) => {\n expect(areBranchesQuoteCompatible(createBranch(), createBranch(override))).toBe(false);\n });\n\n it('treats a missing externalQuote as false rather than as a difference', () => {\n const implicit = createBranch();\n const explicit = createBranch({ externalQuote: false });\n\n expect(areBranchesQuoteCompatible(implicit, explicit)).toBe(true);\n });\n});\n\ndescribe('getBranchAskingVies', () => {\n const bolivia = createBranch({ id: 'BO', country: 'bo', vatNumber: '387032026' });\n const belgium = createBranch({ id: 'BE', country: 'be', vatNumber: 'BE0405567886' });\n const france = createBranch({ id: 'FR', country: 'fr', vatNumber: 'FR10821739638' });\n const tenantOf = (branches: Branch[]) => ({ branches }) as Tenant;\n\n test.each([\n ['the European branch the check is made for', 'FR', 'FR'],\n ['the first European branch when no branch is given', undefined, 'BE'],\n ['the first European branch when the given branch is outside the EU', 'BO', 'BE'],\n ['the first European branch when the given branch is unknown', 'GONE', 'BE'],\n ])('picks %s', (_label, branchId, expected) => {\n expect(getBranchAskingVies(tenantOf([bolivia, belgium, france]), branchId)?.id).toBe(expected);\n });\n\n test('asks anonymously for a tenant without a European branch', () => {\n expect(getBranchAskingVies(tenantOf([bolivia]), 'BO')).toBeUndefined();\n });\n});\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bisondesk/core-sdk",
3
- "version": "1.0.676",
3
+ "version": "1.0.678",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "author": "TheTruckCompany",
@@ -0,0 +1,123 @@
1
+ import { SERVICE_ID_ADMIN_HEADER, TENANT_ID_ADMIN_HEADER } from '@bisondesk/commons-sdk/constants';
2
+ import { XError } from '@bisondesk/commons-sdk/errors';
3
+ import { cleanHeaders, getAdminAuth } from '@bisondesk/commons-sdk/fetch';
4
+ import { DataRecord } from '@bisondesk/commons-sdk/utils';
5
+ import fetch, { Response } from 'node-fetch';
6
+ import {
7
+ NewOrganizationBankAccount,
8
+ OrganizationBankAccount,
9
+ OrganizationBankAccountMeta,
10
+ } from '../types/organization-bank-accounts.js';
11
+
12
+ type ServiceOptions = { serviceId?: string };
13
+
14
+ /**
15
+ * Calls an organisation bank-account route as a service. The service id becomes the user core
16
+ * records as having made the change.
17
+ */
18
+ const callBankAccountsRoute = async (
19
+ tenantId: string,
20
+ path: string,
21
+ init: { method: string; body?: unknown },
22
+ opts?: ServiceOptions,
23
+ ): Promise<Response> => {
24
+ const auth = await getAdminAuth();
25
+ const url = `${process.env.CORE_API_ORIGIN}${path}`;
26
+ const response: Response = await fetch(url, {
27
+ method: init.method,
28
+ headers: cleanHeaders({
29
+ Authorization: auth,
30
+ 'Content-Type': init.body != null ? 'application/json' : undefined,
31
+ [TENANT_ID_ADMIN_HEADER]: tenantId,
32
+ [SERVICE_ID_ADMIN_HEADER]: opts?.serviceId,
33
+ }),
34
+ body: init.body != null ? JSON.stringify(init.body) : undefined,
35
+ });
36
+ if (!response.ok) {
37
+ const body = await response.text();
38
+ throw new XError(response.statusText, { method: init.method, url, body, tenantId, opts });
39
+ }
40
+ return response;
41
+ };
42
+
43
+ const bankAccountsPath = (organizationId: string) =>
44
+ `/api/crm/organizations/${organizationId}/bank-accounts`;
45
+
46
+ /** An organisation's bank accounts, the default first. */
47
+ export const listOrganizationBankAccounts = async (
48
+ tenantId: string,
49
+ organizationId: string,
50
+ opts?: ServiceOptions,
51
+ ): Promise<OrganizationBankAccount[]> => {
52
+ const response = await callBankAccountsRoute(
53
+ tenantId,
54
+ bankAccountsPath(organizationId),
55
+ { method: 'GET' },
56
+ opts,
57
+ );
58
+ const records = (await response.json()) as DataRecord<
59
+ OrganizationBankAccount,
60
+ OrganizationBankAccountMeta
61
+ >[];
62
+ return records.map((entry) => entry.record);
63
+ };
64
+
65
+ export const createOrganizationBankAccount = async (
66
+ tenantId: string,
67
+ input: NewOrganizationBankAccount,
68
+ opts?: ServiceOptions,
69
+ ): Promise<OrganizationBankAccount> => {
70
+ const { organizationId, ...details } = input;
71
+ const response = await callBankAccountsRoute(
72
+ tenantId,
73
+ bankAccountsPath(organizationId),
74
+ { method: 'POST', body: details },
75
+ opts,
76
+ );
77
+ return (await response.json()) as OrganizationBankAccount;
78
+ };
79
+
80
+ export const updateOrganizationBankAccount = async (
81
+ tenantId: string,
82
+ bankAccountId: string,
83
+ input: NewOrganizationBankAccount,
84
+ opts?: ServiceOptions,
85
+ ): Promise<OrganizationBankAccount> => {
86
+ const { organizationId, ...details } = input;
87
+ const response = await callBankAccountsRoute(
88
+ tenantId,
89
+ `${bankAccountsPath(organizationId)}/${bankAccountId}`,
90
+ { method: 'PUT', body: details },
91
+ opts,
92
+ );
93
+ return (await response.json()) as OrganizationBankAccount;
94
+ };
95
+
96
+ export const deleteOrganizationBankAccount = async (
97
+ tenantId: string,
98
+ organizationId: string,
99
+ bankAccountId: string,
100
+ opts?: ServiceOptions,
101
+ ): Promise<void> => {
102
+ await callBankAccountsRoute(
103
+ tenantId,
104
+ `${bankAccountsPath(organizationId)}/${bankAccountId}`,
105
+ { method: 'DELETE' },
106
+ opts,
107
+ );
108
+ };
109
+
110
+ export const setDefaultOrganizationBankAccount = async (
111
+ tenantId: string,
112
+ organizationId: string,
113
+ bankAccountId: string,
114
+ opts?: ServiceOptions,
115
+ ): Promise<OrganizationBankAccount> => {
116
+ const response = await callBankAccountsRoute(
117
+ tenantId,
118
+ `${bankAccountsPath(organizationId)}/${bankAccountId}/default`,
119
+ { method: 'POST' },
120
+ opts,
121
+ );
122
+ return (await response.json()) as OrganizationBankAccount;
123
+ };
@@ -604,6 +604,7 @@ export type HpLeasingContractRequest = {
604
604
  vanCourier: boolean;
605
605
  startDate: number;
606
606
  opportunityContractId: string;
607
+ maxKmPerYear?: number;
607
608
  data: {
608
609
  leasingParameter: {
609
610
  allRisksPercentage: number;
@@ -0,0 +1,56 @@
1
+ import { BankAccountDetails } from '@bisondesk/commons-sdk/bank-accounts';
2
+
3
+ /**
4
+ * An accountant's confirmation of one account number. It only counts while the account still
5
+ * holds that number, so trust never carries over to a new number however the account was edited.
6
+ */
7
+ export type OrganizationBankAccountVerification = {
8
+ accountNumber: string;
9
+ verifiedBy: string;
10
+ verifiedAt: string;
11
+ };
12
+
13
+ export enum OrganizationBankAccountActions {
14
+ VERIFY = 'verify',
15
+ }
16
+
17
+ export enum OrganizationBankAccountActionBlockerCode {
18
+ CHANGED_NEEDS_VERIFICATION = 'changed_needs_verification',
19
+ }
20
+
21
+ export type OrganizationBankAccountActionBlocker = {
22
+ code: OrganizationBankAccountActionBlockerCode;
23
+ };
24
+
25
+ /**
26
+ * Why an action the user would otherwise see is refused right now. A missing permission hides the
27
+ * action instead.
28
+ */
29
+ export type OrganizationBankAccountBlockedAction = {
30
+ action: OrganizationBankAccountActions;
31
+ reasons: OrganizationBankAccountActionBlocker[];
32
+ };
33
+
34
+ export type OrganizationBankAccountMeta = {
35
+ blockedActions: OrganizationBankAccountBlockedAction[];
36
+ };
37
+
38
+ export type NewOrganizationBankAccount = BankAccountDetails & {
39
+ organizationId: string;
40
+ };
41
+
42
+ /**
43
+ * An organisation's bank account for outgoing SEPA transfers, never shared across companies. An
44
+ * organisation can hold several; at most one is its `default`, the one payment exports pay to.
45
+ * Editing the account number or holder name clears the manual verification, so a valid checksum is
46
+ * never mistaken for a confirmed account holder.
47
+ */
48
+ export type OrganizationBankAccount = NewOrganizationBankAccount & {
49
+ id: string;
50
+ default?: boolean;
51
+ createdAt: string;
52
+ createdBy: string;
53
+ modifiedAt: string;
54
+ modifiedBy: string;
55
+ verification?: OrganizationBankAccountVerification;
56
+ };
@@ -0,0 +1,40 @@
1
+ import { BankAccountDetails } from '@bisondesk/commons-sdk/bank-accounts';
2
+ import { AttachmentValue } from '@bisondesk/commons-sdk/types';
3
+
4
+ /**
5
+ * The transfer details of one payment, frozen at export time. A later edit to the payment or the
6
+ * supplier bank account must not rewrite what the sent file said, so the export keeps its own copy
7
+ * of every value that went into the XML. It also records the payment batch the payment was sent to
8
+ * processing in, so the export is a self-contained audit record even though one file can span
9
+ * several batches.
10
+ */
11
+ export type PaymentExportLine = {
12
+ paymentId: string;
13
+ vehicleId: string;
14
+ amount: string;
15
+ creditor: BankAccountDetails;
16
+ paymentBatchId: string;
17
+ transferReference?: string;
18
+ };
19
+
20
+ export type NewPaymentExport = {
21
+ /** Single execution date (`yyyy-MM-dd`) written to the file's `ReqdExctnDt`. */
22
+ executionDate: string;
23
+ /** Σ of the exported line amounts (string; computed with decimal arithmetic). */
24
+ totalAmount: string;
25
+ paymentIds: string[];
26
+ lines: PaymentExportLine[];
27
+ /** The generated SEPA file, stored in S3 and re-downloadable from Processing. */
28
+ xmlFile: AttachmentValue;
29
+ };
30
+
31
+ /**
32
+ * A generated SEPA file for a selection of Processing payments, plus the frozen record of exactly what
33
+ * was sent. Re-downloading serves the same file and never re-exports; a payment cancelled out of the
34
+ * file returns to Allowed and leaves this record intact.
35
+ */
36
+ export type PaymentExport = NewPaymentExport & {
37
+ id: string;
38
+ exportedAt: string;
39
+ exportedBy: string;
40
+ };
@@ -91,6 +91,8 @@ export type LeasingQuoteParams<P extends CountrySpecificQuoteParams = CountrySpe
91
91
  leasingDuration: number;
92
92
  leasingDepositPercentage: string;
93
93
  leasingResidualValuePercentage: string;
94
+ /** Seeds `leasing.maxKmPerYear` on new quotes. */
95
+ maxKmPerYear?: number;
94
96
  };
95
97
  costs: {
96
98
  bankDepositPercentage: string;
@@ -91,6 +91,8 @@ export type LeasingCalculatorInput = BaseQuoteCalculatorInput & {
91
91
  interestRate: string;
92
92
  duration: number;
93
93
  contractStartDate: string;
94
+ /** Mileage cap the contract allows per year, in km. Printed on the quote and the contract. */
95
+ maxKmPerYear?: number;
94
96
  };
95
97
  bank: {
96
98
  interestRate: string;
@@ -55,6 +55,13 @@ export enum TenantModule {
55
55
  Intercompany = 'intercompany',
56
56
  Opportunities = 'opportunities',
57
57
  Offers = 'offers',
58
+ /**
59
+ * The tenant pays suppliers through Isabel file exports (SEPA transfer files). It grants the
60
+ * export itself and the verification of the supplier bank accounts it pays to, and requires those
61
+ * account numbers to be valid IBANs. Without it, Processing payments are simply marked paid, and
62
+ * a bank account can hold any account number.
63
+ */
64
+ PaymentExports = 'payment_exports',
58
65
  Vehicles = 'vehicles',
59
66
  Tasks = 'tasks',
60
67
  TrackAndTrace = 'track_and_trace',
@@ -1,9 +1,23 @@
1
1
  import { BaseEvent } from './internal-events.js';
2
+ import { OrganizationBankAccountActions } from './organization-bank-accounts.js';
2
3
  import { SearchVehicle } from './vehicles.js';
3
4
 
4
5
  export const VEHICLE_PURCHASE_PAYMENT_ALLOWED_NOTIFICATION_ORIGIN =
5
6
  'vehicle-purchase-payment-allowed-notification';
6
7
 
8
+ /**
9
+ * The stage a supplier payment sits at, driving the Unpaid/Allowed/Processing/Executed tabs.
10
+ * A payment only ever moves forward: an authorised payment is `allowed`; sending it to processing
11
+ * groups it into a payment batch and hands it to accounting (`processing`), which exports it
12
+ * to the Isabel bank file; marking it paid moves it to `executed`.
13
+ * (`unpaid` is not a stage — it is an agreed purchase with an amount still to authorise.)
14
+ */
15
+ export enum PaymentStage {
16
+ Allowed = 'allowed',
17
+ Processing = 'processing',
18
+ Executed = 'executed',
19
+ }
20
+
7
21
  export type NewVehiclePurchasePayment = {
8
22
  vehicleId: string;
9
23
  amount: string;
@@ -16,11 +30,113 @@ export type VehiclePurchasePayment = NewVehiclePurchasePayment & {
16
30
  id: string;
17
31
  createdBy: string;
18
32
  createdAt: string;
33
+ stage: PaymentStage;
34
+ /** Manual settlement priority within the Allowed tab; lower is paid first. */
35
+ priority: number;
36
+ /** Set once the payment has been exported; links it to its payment export. */
37
+ exportId?: string;
38
+ /** The payment batch this payment was sent to processing in; unset while Allowed, and on payments
39
+ * marked paid straight from Allowed. */
40
+ paymentBatchId?: string;
41
+ /**
42
+ * Supplier-facing communication that becomes the SEPA transfer's remittance information (`RmtInf`),
43
+ * so the supplier can reconcile the payment to an invoice. Editable while Processing and not yet
44
+ * exported; prefilled from the latest purchase invoice. This is not the file's per-line
45
+ * `EndToEndId`.
46
+ */
47
+ transferReference?: string;
48
+ /** Who sent the payment to processing, and when — set at the Allowed → Processing step. */
49
+ sentToProcessingBy?: string;
50
+ sentToProcessingAt?: string;
51
+ };
52
+
53
+ /**
54
+ * One send to processing: the Allowed payments an accountant moved to Processing together, and who
55
+ * sent them. The Processing tab groups its rows by batch. A batch holds no amount of its own; its
56
+ * total is the sum of the payments still in it.
57
+ */
58
+ export type PaymentBatch = {
59
+ id: string;
60
+ createdAt: string;
61
+ createdBy: string;
62
+ };
63
+
64
+ /**
65
+ * Whether a payment's supplier can be paid in an export. Mirrors the export gate: a transfer needs a
66
+ * supplier bank account that carries an IBAN and holder name and has been verified. Shown as an
67
+ * export-blocked marker on the Processing rows, where the accountant clears it before exporting.
68
+ * (The invoice / pro-forma document half of the gate is not reflected here — its source is a Q3
69
+ * residual the backend export gate also defers.)
70
+ *
71
+ * The bank account is the supplier's default one, `bankAccountId`, which is the account an export
72
+ * pays. `bankAccountActions` lists what the current user may do on it, so a row can point out the
73
+ * accounts they can verify themselves. Once the account number or holder name changed, verify is
74
+ * left out for accountants, and for whichever Senior Accountant made the change (four-eyes).
75
+ */
76
+ export type PaymentExportChecks = {
77
+ bankAccountId?: string;
78
+ hasBankAccount: boolean;
79
+ bankAccountVerified: boolean;
80
+ bankAccountActions: OrganizationBankAccountActions[];
19
81
  };
20
82
 
21
83
  export type VehiclePurchasePaymentBff = {
22
84
  payment: VehiclePurchasePayment;
23
85
  vehicle: SearchVehicle;
86
+ /** Name of the supplier organisation (`vehicle.purchase.organizationId`), resolved for display. */
87
+ supplierName?: string;
88
+ /** Export checks on the supplier bank account; filled for the Processing list only. */
89
+ exportChecks?: PaymentExportChecks;
90
+ };
91
+
92
+ /**
93
+ * A row on the Unpaid tab: an agreed purchase that still has an amount to authorise for payment.
94
+ * Derived from the purchase deal and the payments already authorised, not a payment record.
95
+ */
96
+ export type UnpaidPurchase = {
97
+ vehicle: SearchVehicle;
98
+ /** The agreed purchase price including VAT (`deal.purchase` grossed up by `deal.purchaseVatRate`),
99
+ * so it matches the VAT-inclusive cost the vehicle purchase tab authorises against. */
100
+ purchaseAmount: string;
101
+ /** Sum of amounts already authorised (payments in any stage) for this vehicle. */
102
+ allowedAmount: string;
103
+ /** Sum of the authorised amounts already settled (payments in Executed) for this vehicle. */
104
+ paidAmount: string;
105
+ /** `purchaseAmount − allowedAmount`; always greater than zero for rows shown here. */
106
+ remainingAmount: string;
107
+ /** Due date of the purchase invoice. */
108
+ invoiceDueDate?: string;
109
+ /** Name of the supplier organisation (`vehicle.purchase.organizationId`), resolved for display. */
110
+ supplierName?: string;
111
+ };
112
+
113
+ /** Body of the export request: the Processing payments to put in one file, the paying account, and the execution date. */
114
+ export type ExportPaymentsRequest = {
115
+ paymentIds: string[];
116
+ /** The bank account (from documents settings) to pay the file from — the file's debtor. */
117
+ debtorBankAccountId: string;
118
+ /** Single execution date (`yyyy-MM-dd`) written to `ReqdExctnDt`; defaults to today. */
119
+ executionDate?: string;
120
+ };
121
+
122
+ /** Body of the mark-paid request: the payments (Allowed or Processing) to settle in one call. */
123
+ export type MarkPaymentsPaidRequest = {
124
+ paymentIds: string[];
125
+ };
126
+
127
+ /** Body of the send-to-processing request: the Allowed payments to send together as one batch. */
128
+ export type SendPaymentsToProcessingRequest = {
129
+ paymentIds: string[];
130
+ };
131
+
132
+ /**
133
+ * How many rows sit behind each payment tab, for the tab counters. Executed is unbounded and has no
134
+ * counter, so it is left out.
135
+ */
136
+ export type PurchasePaymentCounts = {
137
+ unpaid: number;
138
+ allowed: number;
139
+ processing: number;
24
140
  };
25
141
 
26
142
  export type VehiclePurchasePaymentCreateEvent = {
@@ -846,6 +846,9 @@ export type VehiclePurchase = {
846
846
  checkedInBy?: string;
847
847
  };
848
848
 
849
+ /** Due date of the supplier purchase invoice; editable from the payments page. */
850
+ invoiceDueDate?: string;
851
+
849
852
  deal: {
850
853
  status: VehiclePurchaseDealStatus; // partially set by the user, if there are payments then it is set automatically by the backend
851
854
  readyToDeliver?: boolean;
@@ -0,0 +1,19 @@
1
+ import { normalizeIban } from '@bisondesk/commons-sdk/iban';
2
+ import { OrganizationBankAccount } from '../types/organization-bank-accounts.js';
3
+
4
+ /**
5
+ * Whether an organisation's bank account can be paid: it carries a verification, and that
6
+ * verification was given for the account number the account holds now. A verification recorded
7
+ * without an account number, or for a different one, does not count.
8
+ *
9
+ * Shared so the export gate and the Processing rows read one rule.
10
+ */
11
+ export const isOrganizationBankAccountVerified = (
12
+ account: OrganizationBankAccount | undefined,
13
+ ): boolean => {
14
+ const verifiedAccountNumber = account?.verification?.accountNumber;
15
+ if (account == null || verifiedAccountNumber == null) {
16
+ return false;
17
+ }
18
+ return normalizeIban(verifiedAccountNumber) === normalizeIban(account.accountNumber);
19
+ };
@@ -1,5 +1,5 @@
1
- import { Branch } from '../types/tenants.js';
2
- import { areBranchesQuoteCompatible } from './tenants.js';
1
+ import { Branch, Tenant } from '../types/tenants.js';
2
+ import { areBranchesQuoteCompatible, getBranchAskingVies } from './tenants.js';
3
3
 
4
4
  const createBranch = (override?: Partial<Branch>): Branch => ({
5
5
  id: 'TTC',
@@ -45,3 +45,23 @@ describe('areBranchesQuoteCompatible', () => {
45
45
  expect(areBranchesQuoteCompatible(implicit, explicit)).toBe(true);
46
46
  });
47
47
  });
48
+
49
+ describe('getBranchAskingVies', () => {
50
+ const bolivia = createBranch({ id: 'BO', country: 'bo', vatNumber: '387032026' });
51
+ const belgium = createBranch({ id: 'BE', country: 'be', vatNumber: 'BE0405567886' });
52
+ const france = createBranch({ id: 'FR', country: 'fr', vatNumber: 'FR10821739638' });
53
+ const tenantOf = (branches: Branch[]) => ({ branches }) as Tenant;
54
+
55
+ test.each([
56
+ ['the European branch the check is made for', 'FR', 'FR'],
57
+ ['the first European branch when no branch is given', undefined, 'BE'],
58
+ ['the first European branch when the given branch is outside the EU', 'BO', 'BE'],
59
+ ['the first European branch when the given branch is unknown', 'GONE', 'BE'],
60
+ ])('picks %s', (_label, branchId, expected) => {
61
+ expect(getBranchAskingVies(tenantOf([bolivia, belgium, france]), branchId)?.id).toBe(expected);
62
+ });
63
+
64
+ test('asks anonymously for a tenant without a European branch', () => {
65
+ expect(getBranchAskingVies(tenantOf([bolivia]), 'BO')).toBeUndefined();
66
+ });
67
+ });
@@ -1,4 +1,5 @@
1
1
  import { XError } from '@bisondesk/commons-sdk/errors';
2
+ import { isEUCountry } from '@bisondesk/commons-sdk/locations';
2
3
  import { first } from 'lodash-es';
3
4
  import { Branch, Tenant } from '../types/tenants.js';
4
5
 
@@ -13,6 +14,16 @@ export const getBranch = (tenant: Tenant, branchId: string): Branch => {
13
14
 
14
15
  export const getDefaultBranch = (tenant: Tenant): Branch => first(tenant.branches)!;
15
16
 
17
+ /**
18
+ * The branch whose VAT number VIES records as the requester of a VAT check, which earns a
19
+ * consultation number: the branch the check is made for when it is European, else the tenant's
20
+ * first European branch. A tenant without a European branch asks VIES anonymously.
21
+ */
22
+ export const getBranchAskingVies = (tenant: Tenant, branchId?: string): Branch | undefined => {
23
+ const europeanBranches = tenant.branches.filter((branch) => isEUCountry(branch.country));
24
+ return europeanBranches.find((branch) => branch.id === branchId) ?? europeanBranches.at(0);
25
+ };
26
+
16
27
  /** The source invoices the end customer directly; this branch collects payment and earns
17
28
  * commission (see {@link Branch.dropshipping}). */
18
29
  export const isDropshippingBranch = (branch: Branch): boolean => branch.dropshipping === true;