@bisondesk/core-sdk 1.0.674 → 1.0.676
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.
- package/lib/constants.d.ts +1 -0
- package/lib/constants.d.ts.map +1 -1
- package/lib/constants.js +1 -0
- package/lib/constants.js.map +1 -1
- package/lib/types/crm.d.ts +13 -8
- package/lib/types/crm.d.ts.map +1 -1
- package/lib/types/crm.js +8 -2
- package/lib/types/crm.js.map +1 -1
- package/lib/types/deliveries.d.ts.map +1 -1
- package/lib/types/deliveries.js.map +1 -1
- package/lib/types/leasing-debtors.d.ts +11 -0
- package/lib/types/leasing-debtors.d.ts.map +1 -1
- package/lib/types/leasing-debtors.js.map +1 -1
- package/lib/types/tenants.d.ts +2 -0
- package/lib/types/tenants.d.ts.map +1 -1
- package/lib/types/tenants.js +1 -0
- package/lib/types/tenants.js.map +1 -1
- package/lib/utils/marketing-opt-outs.d.ts +10 -0
- package/lib/utils/marketing-opt-outs.d.ts.map +1 -0
- package/lib/utils/marketing-opt-outs.js +8 -0
- package/lib/utils/marketing-opt-outs.js.map +1 -0
- package/lib/utils/marketing-opt-outs.test.d.ts +2 -0
- package/lib/utils/marketing-opt-outs.test.d.ts.map +1 -0
- package/lib/utils/marketing-opt-outs.test.js +22 -0
- package/lib/utils/marketing-opt-outs.test.js.map +1 -0
- package/lib/utils/organizations.d.ts.map +1 -1
- package/lib/utils/organizations.js.map +1 -1
- package/package.json +1 -1
- package/src/constants.ts +2 -0
- package/src/types/crm.ts +30 -9
- package/src/types/deliveries.ts +2 -2
- package/src/types/leasing-debtors.ts +14 -0
- package/src/types/tenants.ts +12 -0
- package/src/utils/marketing-opt-outs.test.ts +34 -0
- package/src/utils/marketing-opt-outs.ts +20 -0
- package/src/utils/organizations.ts +1 -2
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tenants.d.ts","sourceRoot":"/","sources":["types/tenants.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,8BAA8B,CAAC;AAClF,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAElD,MAAM,MAAM,YAAY,GAAG;IACzB,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,CAAC,MAAM,EAAE,GAAG,MAAM,EAAE,CAAC,CAAC;CAInC,CAAC;AAEF,oBAAY,YAAY;IACtB,cAAc,mBAAmB;IACjC,GAAG,QAAQ;IAOX,UAAU,gBAAgB;IAC1B,GAAG,QAAQ;IACX,IAAI,SAAS;IAMb,UAAU,gBAAgB;IAQ1B,WAAW,iBAAiB;IAC5B,KAAK,UAAU;IACf,OAAO,YAAY;IACnB,yBAAyB,gCAAgC;IACzD,SAAS,cAAc;IACvB,QAAQ,aAAa;IAQrB,YAAY,iBAAiB;IAC7B,aAAa,kBAAkB;IAC/B,MAAM,WAAW;IACjB,QAAQ,aAAa;IACrB,KAAK,UAAU;IACf,aAAa,oBAAoB;IACjC,SAAS,cAAc;IACvB,aAAa,mBAAmB;
|
|
1
|
+
{"version":3,"file":"tenants.d.ts","sourceRoot":"/","sources":["types/tenants.ts"],"names":[],"mappings":"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"}
|
package/lib/types/tenants.js
CHANGED
|
@@ -20,6 +20,7 @@ export var TenantModule;
|
|
|
20
20
|
TenantModule["TrackAndTrace"] = "track_and_trace";
|
|
21
21
|
TenantModule["Bootstrap"] = "bootstrap";
|
|
22
22
|
TenantModule["WhatsappCrisp"] = "whatsapp_crisp";
|
|
23
|
+
TenantModule["WhatsappNotifications"] = "whatsapp_notifications";
|
|
23
24
|
TenantModule["Hexon"] = "hexon";
|
|
24
25
|
TenantModule["DeliveryConfirmation"] = "delivery_confirmation";
|
|
25
26
|
TenantModule["Transport"] = "transport";
|
package/lib/types/tenants.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tenants.js","sourceRoot":"/","sources":["types/tenants.ts"],"names":[],"mappings":"AAgBA,MAAM,CAAN,IAAY,
|
|
1
|
+
{"version":3,"file":"tenants.js","sourceRoot":"/","sources":["types/tenants.ts"],"names":[],"mappings":"AAgBA,MAAM,CAAN,IAAY,YAyDX;AAzDD,WAAY,YAAY;IACtB,iDAAiC,CAAA;IACjC,2BAAW,CAAA;IAOX,0CAA0B,CAAA;IAC1B,2BAAW,CAAA;IACX,6BAAa,CAAA;IAMb,0CAA0B,CAAA;IAQ1B,4CAA4B,CAAA;IAC5B,+BAAe,CAAA;IACf,mCAAmB,CAAA;IACnB,yEAAyD,CAAA;IACzD,uCAAuB,CAAA;IACvB,qCAAqB,CAAA;IAQrB,6CAA6B,CAAA;IAC7B,+CAA+B,CAAA;IAC/B,iCAAiB,CAAA;IACjB,qCAAqB,CAAA;IACrB,+BAAe,CAAA;IACf,iDAAiC,CAAA;IACjC,uCAAuB,CAAA;IACvB,gDAAgC,CAAA;IAMhC,gEAAgD,CAAA;IAChD,+BAAe,CAAA;IACf,8DAA8C,CAAA;IAC9C,uCAAuB,CAAA;IACvB,mCAAmB,CAAA;IACnB,uCAAuB,CAAA;AACzB,CAAC,EAzDW,YAAY,KAAZ,YAAY,QAyDvB;AAGD,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,UAAU,CAAU,CAAC","sourcesContent":["import { CognitoInfo, LocationValue, Region } from '@bisondesk/commons-sdk/types';\nimport { OpportunityType } from '../constants.js';\n\nexport type PublicTenant = {\n defaultCountry: string;\n defaultCurrency: string;\n defaultLanguage: string;\n id: string; // the id is provided on creation bc we assume tenants will be created by someone on the team\n name: string;\n region: Region;\n subdomains: [string, ...string[]];\n\n // Pay attention to what information is added here since\n // the tenant can be obtained without authentication\n};\n\nexport enum TenantModule {\n Administration = 'administration',\n App = 'app',\n /**\n * The tenant's users can put the app into client mode, hiding the internal-only pages while\n * showing a screen to a customer. Left off for a tenant whose users are themselves the\n * customer: it would hide their sign-out to no purpose, and turning it back off needs a PIN\n * they were never given.\n */\n ClientMode = 'client_mode',\n CRM = 'crm',\n Docs = 'docs',\n /**\n * The app may send email on behalf of this tenant's users, e.g. sharing a vehicle: the\n * sender is the requesting user's own address, so the tenant's email domain must be one we\n * can deliver from. Absent on client tenants — we do not send mail as outsiders.\n */\n EmailShare = 'email_share',\n /**\n * The tenant's users can sign in with Google. It is a tenant fact rather than a user one:\n * `googleEmail` is only filled when the Google address differs from the BisonDesk one, and\n * every pool carries the Google provider regardless, so neither can tell who actually uses it.\n * Absent means password-only, which is what routes those users an emailed invite with a\n * genuine temporary password.\n */\n GoogleLogin = 'google_login',\n Leads = 'leads',\n Leasing = 'leasing',\n LeasingContractGeneration = 'leasing_contract_generation',\n Marketing = 'marketing',\n Insights = 'insights',\n /**\n * The tenant has agreed to exchange deal data through BisonDesk automatically, on top of the\n * bare vehicle distribution: it may be named as the buyer on an intercompany sale, which lets\n * the invoice sync and the purchase reconciliation write a purchase onto its copy. Read on the\n * BUYER only. All group tenants have it today; a supplier or client tenant that wants deal\n * data pushed too joins the exchange by being granted it.\n */\n Intercompany = 'intercompany',\n Opportunities = 'opportunities',\n Offers = 'offers',\n Vehicles = 'vehicles',\n Tasks = 'tasks',\n TrackAndTrace = 'track_and_trace',\n Bootstrap = 'bootstrap',\n WhatsappCrisp = 'whatsapp_crisp',\n /**\n * The app sends leasing clients WhatsApp messages on its own: the monthly invoice summary and\n * the overdue payment reminders (ADR-0026). Separate from `whatsapp_crisp`, which only lets\n * users send WhatsApp messages themselves. Both flows also need the tenant's Crisp settings.\n */\n WhatsappNotifications = 'whatsapp_notifications',\n Hexon = 'hexon',\n DeliveryConfirmation = 'delivery_confirmation',\n Transport = 'transport',\n Reports = 'reports',\n Prospects = 'prospects',\n}\n\n/** Report tabs a tenant can enable, in canonical order. */\nexport const REPORT_TAB_KEYS = ['sales', 'leasing', 'purchases', 'arrivals'] as const;\n\nexport type ReportTabKey = (typeof REPORT_TAB_KEYS)[number];\n\nexport type Tenant = PublicTenant & {\n cognito?: CognitoInfo;\n languages: [string, ...string[]];\n conglomerate?: string;\n emailDomain: string;\n hyperdmsIds?: string[];\n hyperportalId?: string;\n\n /* deprecated */\n exactAccounting?: boolean;\n\n branches: [Branch, ...Branch[]];\n modules: TenantModule[];\n /**\n * Reports page configuration. `tabs` lists the visible tabs in display order;\n * absent or empty means no reports are configured for the tenant (the page\n * shows a notice instead of tabs).\n */\n reports?: {\n tabs: ReportTabKey[];\n };\n test?: boolean;\n opportunities: {\n types: OpportunityType[];\n };\n /**\n * Reservation expiry for this tenant's OWN vehicles (ADR-0026): the truck's owner sets the\n * clock, wherever the deal runs — for a cross-tenant copy the chain origin's policy\n * applies, whatever opportunity type the tenant selling to the end customer uses.\n * Absent = the default\n * business-day tiers; the tier is still picked by the local customer org's\n * trustworthy/conglomerate flags.\n */\n reservationExpiry?: ReservationExpiryPolicy;\n crm?: {\n vatNumberRequiredForCompanies?: boolean;\n };\n accounting?: {\n costCenterPerLine?: boolean;\n };\n // E.164 Twilio caller ID used as a fallback when a user has no own number configured\n defaultTwilioPhoneNumber?: string;\n deliveryEmail?: string;\n /**\n * The tenant's brand. The vehicle sticker's header band takes `primary`, and so do document\n * headings when the branch letterhead sets no colour, so one layout serves every tenant. The\n * app's navigation shows the tenant name on `primary`, so users working in several tenants\n * can tell them apart. Absent = neutral grey.\n */\n brand?: {\n /** `#rrggbb` */\n primary: string;\n };\n};\n\nexport type ReservationExpiryPolicy =\n | { policy: 'disabled' }\n | { policy: 'days'; generic: number; trustworthy: number; conglomerate: number };\n\nexport type AccountingProvider = 'exact' | 'odoo';\n\nexport type Branch = {\n id: string;\n name: string;\n country: string;\n language: string;\n /* deprecated — use `accounting === 'exact'` instead. Kept until prod cutover. */\n exactAccounting?: boolean;\n accounting?: AccountingProvider;\n currency: string;\n adminNumberPrefix: string;\n stockNumberPrefix: string;\n vatNumber: string;\n contactPerson?: { firstName: string; lastName: string };\n vatRates: {\n standard: string; //percentage value (e.g. 21)\n };\n location: LocationValue;\n /**\n * The parking (a `location` picklist value) this branch's vehicles go to when their purchase\n * names none. Vehicle entry offers it as the stock destination, and the transport ITR prints\n * it as the park. Absent = the ITR prints the branch's own address.\n */\n defaultParking?: string;\n\n storecoveExternalId?: number;\n // true if the branch is for a company where VAT is due when the invoice\n // is issued not when the payment is collected (e.g. France, Italy, Spain)\n vatOnDebits?: boolean;\n /**\n * The French issuing duties — e-invoicing, e-reporting and the Encaissée status — are switched\n * on for this branch. Receiving is unaffected and always active.\n *\n * Kept separate from `storecoveExternalId` on purpose: registering a Storecove legal entity only\n * makes a branch reachable, it does not mean the French duties have started. As a PME those\n * duties begin 2027-09-01, so this stays off until then or until a deliberate earlier adoption.\n */\n frIssuingEnabled?: boolean;\n externalQuote?: boolean;\n hyperdmsId?: string;\n internalDealsEnabled?: boolean;\n /** The source invoices the end customer directly; this branch collects payment and earns\n * commission. Intercompany source opportunities for this branch's deals take the buyer\n * deal's own client instead of the branch counterparty org. */\n dropshipping?: boolean;\n};\n\nexport type WhatsappStateResponse = {\n enabled: boolean;\n};\n\n/**\n * What the login page is told about a tenant before anyone has signed in. Reached by subdomain\n * on the one public tenant endpoint, so it carries only what an anonymous visitor may see —\n * mind that when adding to it.\n *\n * The two booleans are answers the page cannot work out for itself: it has no session, so no\n * modules to read. They say which sign-in options to offer and whether WhatsApp sharing is on.\n */\nexport type TenantInfoResponse = {\n tenant: PublicTenant;\n whatsapp: boolean;\n googleLogin: boolean;\n cognito: CognitoInfo;\n};\n"]}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { MarketingChannel } from '../types/crm.js';
|
|
2
|
+
export declare const normalizeEmail: (email: string) => string;
|
|
3
|
+
export declare const normalizeMarketingAddress: (channel: MarketingChannel, address: string) => string;
|
|
4
|
+
export declare const getContactMarketingAddresses: (contact: {
|
|
5
|
+
emails: string[];
|
|
6
|
+
phones: {
|
|
7
|
+
e164: string;
|
|
8
|
+
}[];
|
|
9
|
+
}) => Record<MarketingChannel, string[]>;
|
|
10
|
+
//# sourceMappingURL=marketing-opt-outs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"marketing-opt-outs.d.ts","sourceRoot":"/","sources":["utils/marketing-opt-outs.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAGnD,eAAO,MAAM,cAAc,UAAW,MAAM,KAAG,MAAoC,CAAC;AAMpF,eAAO,MAAM,yBAAyB,YAAa,gBAAgB,WAAW,MAAM,KAAG,MACR,CAAC;AAGhF,eAAO,MAAM,4BAA4B,YAAa;IACpD,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAC5B,KAAG,MAAM,CAAC,gBAAgB,EAAE,MAAM,EAAE,CAGnC,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { MarketingChannel } from '../types/crm.js';
|
|
2
|
+
export const normalizeEmail = (email) => email.trim().toLowerCase();
|
|
3
|
+
export const normalizeMarketingAddress = (channel, address) => channel === MarketingChannel.Email ? normalizeEmail(address) : address.trim();
|
|
4
|
+
export const getContactMarketingAddresses = (contact) => ({
|
|
5
|
+
[MarketingChannel.Email]: contact.emails,
|
|
6
|
+
[MarketingChannel.Phone]: contact.phones.map((phone) => phone.e164),
|
|
7
|
+
});
|
|
8
|
+
//# sourceMappingURL=marketing-opt-outs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"marketing-opt-outs.js","sourceRoot":"/","sources":["utils/marketing-opt-outs.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAGnD,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,KAAa,EAAU,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;AAMpF,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,OAAyB,EAAE,OAAe,EAAU,EAAE,CAC9F,OAAO,KAAK,gBAAgB,CAAC,KAAK,CAAC,CAAC,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;AAGhF,MAAM,CAAC,MAAM,4BAA4B,GAAG,CAAC,OAG5C,EAAsC,EAAE,CAAC,CAAC;IACzC,CAAC,gBAAgB,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC,MAAM;IACxC,CAAC,gBAAgB,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC;CACpE,CAAC,CAAC","sourcesContent":["import { MarketingChannel } from '../types/crm.js';\n\n/** The form marketing email addresses are stored and compared in: trimmed and lowercased. */\nexport const normalizeEmail = (email: string): string => email.trim().toLowerCase();\n\n/**\n * The form an opted-out address is stored and compared in: email addresses through\n * `normalizeEmail`, phone numbers as the E.164 string contacts already store.\n */\nexport const normalizeMarketingAddress = (channel: MarketingChannel, address: string): string =>\n channel === MarketingChannel.Email ? normalizeEmail(address) : address.trim();\n\n/** A contact's addresses per marketing channel: its email addresses and its phone numbers. */\nexport const getContactMarketingAddresses = (contact: {\n emails: string[];\n phones: { e164: string }[];\n}): Record<MarketingChannel, string[]> => ({\n [MarketingChannel.Email]: contact.emails,\n [MarketingChannel.Phone]: contact.phones.map((phone) => phone.e164),\n});\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"marketing-opt-outs.test.d.ts","sourceRoot":"/","sources":["utils/marketing-opt-outs.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { MarketingChannel } from '@bisondesk/core-sdk/types/crm';
|
|
2
|
+
import { describe, expect, test } from 'vitest';
|
|
3
|
+
import { getContactMarketingAddresses, normalizeEmail, normalizeMarketingAddress, } from './marketing-opt-outs.js';
|
|
4
|
+
describe('normalizeMarketingAddress', () => {
|
|
5
|
+
test('trims and lowercases an email address', () => {
|
|
6
|
+
expect(normalizeEmail(' Buyer@Example.com ')).toBe('buyer@example.com');
|
|
7
|
+
expect(normalizeMarketingAddress(MarketingChannel.Email, ' Buyer@Example.com ')).toBe('buyer@example.com');
|
|
8
|
+
});
|
|
9
|
+
test('keeps a phone number as its E.164 string', () => {
|
|
10
|
+
expect(normalizeMarketingAddress(MarketingChannel.Phone, ' +32470123456 ')).toBe('+32470123456');
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
test('lists a contact’s email addresses and phone numbers per channel', () => {
|
|
14
|
+
expect(getContactMarketingAddresses({
|
|
15
|
+
emails: ['buyer@example.com'],
|
|
16
|
+
phones: [{ e164: '+32470123456' }, { e164: '+32470999999' }],
|
|
17
|
+
})).toEqual({
|
|
18
|
+
[MarketingChannel.Email]: ['buyer@example.com'],
|
|
19
|
+
[MarketingChannel.Phone]: ['+32470123456', '+32470999999'],
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
//# sourceMappingURL=marketing-opt-outs.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"marketing-opt-outs.test.js","sourceRoot":"/","sources":["utils/marketing-opt-outs.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACjE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAChD,OAAO,EACL,4BAA4B,EAC5B,cAAc,EACd,yBAAyB,GAC1B,MAAM,yBAAyB,CAAC;AAEjC,QAAQ,CAAC,2BAA2B,EAAE,GAAG,EAAE;IACzC,IAAI,CAAC,uCAAuC,EAAE,GAAG,EAAE;QACjD,MAAM,CAAC,cAAc,CAAC,qBAAqB,CAAC,CAAC,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAC;QACxE,MAAM,CAAC,yBAAyB,CAAC,gBAAgB,CAAC,KAAK,EAAE,qBAAqB,CAAC,CAAC,CAAC,IAAI,CACnF,mBAAmB,CACpB,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,0CAA0C,EAAE,GAAG,EAAE;QACpD,MAAM,CAAC,yBAAyB,CAAC,gBAAgB,CAAC,KAAK,EAAE,gBAAgB,CAAC,CAAC,CAAC,IAAI,CAC9E,cAAc,CACf,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,iEAAiE,EAAE,GAAG,EAAE;IAC3E,MAAM,CACJ,4BAA4B,CAAC;QAC3B,MAAM,EAAE,CAAC,mBAAmB,CAAC;QAC7B,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,cAAc,EAAE,EAAE,EAAE,IAAI,EAAE,cAAc,EAAE,CAAC;KAC7D,CAAC,CACH,CAAC,OAAO,CAAC;QACR,CAAC,gBAAgB,CAAC,KAAK,CAAC,EAAE,CAAC,mBAAmB,CAAC;QAC/C,CAAC,gBAAgB,CAAC,KAAK,CAAC,EAAE,CAAC,cAAc,EAAE,cAAc,CAAC;KAC3D,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["import { MarketingChannel } from '@bisondesk/core-sdk/types/crm';\nimport { describe, expect, test } from 'vitest';\nimport {\n getContactMarketingAddresses,\n normalizeEmail,\n normalizeMarketingAddress,\n} from './marketing-opt-outs.js';\n\ndescribe('normalizeMarketingAddress', () => {\n test('trims and lowercases an email address', () => {\n expect(normalizeEmail(' Buyer@Example.com ')).toBe('buyer@example.com');\n expect(normalizeMarketingAddress(MarketingChannel.Email, ' Buyer@Example.com ')).toBe(\n 'buyer@example.com',\n );\n });\n\n test('keeps a phone number as its E.164 string', () => {\n expect(normalizeMarketingAddress(MarketingChannel.Phone, ' +32470123456 ')).toBe(\n '+32470123456',\n );\n });\n});\n\ntest('lists a contact’s email addresses and phone numbers per channel', () => {\n expect(\n getContactMarketingAddresses({\n emails: ['buyer@example.com'],\n phones: [{ e164: '+32470123456' }, { e164: '+32470999999' }],\n }),\n ).toEqual({\n [MarketingChannel.Email]: ['buyer@example.com'],\n [MarketingChannel.Phone]: ['+32470123456', '+32470999999'],\n });\n});\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"organizations.d.ts","sourceRoot":"/","sources":["utils/organizations.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kCAAkC,CAAC;
|
|
1
|
+
{"version":3,"file":"organizations.d.ts","sourceRoot":"/","sources":["utils/organizations.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kCAAkC,CAAC;AAmB5D,eAAO,MAAM,mBAAmB,cACnB,MAAM,GAAG,SAAS,UACtB,QAAQ,EAAE,KAChB,OAMF,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"organizations.js","sourceRoot":"/","sources":["utils/organizations.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kCAAkC,CAAC;AAC5D,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAE/C,MAAM,wBAAwB,GAAG,CAAC,CAAC;AAEnC,MAAM,uBAAuB,GAAe;IAC1C,QAAQ,CAAC,cAAc;IACvB,QAAQ,CAAC,cAAc;IACvB,QAAQ,CAAC,KAAK;CACf,CAAC;
|
|
1
|
+
{"version":3,"file":"organizations.js","sourceRoot":"/","sources":["utils/organizations.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kCAAkC,CAAC;AAC5D,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAE/C,MAAM,wBAAwB,GAAG,CAAC,CAAC;AAEnC,MAAM,uBAAuB,GAAe;IAC1C,QAAQ,CAAC,cAAc;IACvB,QAAQ,CAAC,cAAc;IACvB,QAAQ,CAAC,KAAK;CACf,CAAC;AAUF,MAAM,CAAC,MAAM,mBAAmB,GAAG,CACjC,SAA6B,EAC7B,KAAK,GAAe,EAAE,EACb,EAAE;IACX,IAAI,SAAS,IAAI,IAAI,IAAI,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,uBAAuB,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;QACtF,OAAO,KAAK,CAAC;IACf,CAAC;IAED,OAAO,QAAQ,CAAC,SAAS,CAAC,IAAI,IAAI,CAAC,SAAS,CAAC,EAAE,wBAAwB,CAAC,EAAE,IAAI,IAAI,EAAE,CAAC,CAAC;AACxF,CAAC,CAAC","sourcesContent":["import { AppRoles } from '@bisondesk/commons-sdk/constants';\nimport { addMonths, isBefore } from 'date-fns';\n\nconst ORG_IDENTITY_LOCK_MONTHS = 1;\n\nconst ORG_IDENTITY_EDIT_ROLES: AppRoles[] = [\n AppRoles.Administrative,\n AppRoles.GeneralManager,\n AppRoles.Admin,\n];\n\n/**\n * An organization's name and VAT number are its legal identity: the values invoices and contracts\n * are issued against. They stay open for correction right after creation, then freeze for everyone\n * except ORG_IDENTITY_EDIT_ROLES — by then they are already printed on legal documents.\n *\n * Shared so the server guard and the form's disabled state read one rule. Fails open on a missing\n * createdAt, so a new org stays editable in the form until its first save.\n */\nexport const isOrgIdentityLocked = (\n createdAt: string | undefined,\n roles: AppRoles[] = [],\n): boolean => {\n if (createdAt == null || roles.some((role) => ORG_IDENTITY_EDIT_ROLES.includes(role))) {\n return false;\n }\n\n return isBefore(addMonths(new Date(createdAt), ORG_IDENTITY_LOCK_MONTHS), new Date());\n};\n"]}
|
package/package.json
CHANGED
package/src/constants.ts
CHANGED
|
@@ -66,6 +66,8 @@ export const WEBSITE_MARKETING_PLATFORM_ID = 'PUBLIC_WEBSITE';
|
|
|
66
66
|
export const HIDDEN_IN_WEBSITE_MARKETING_PLATFORM_ID = 'HIDDEN_WEBSITE';
|
|
67
67
|
/** Value of CRM_ORG_TAGS_PICKLIST_ID: the organizations a transport can hire as external services. */
|
|
68
68
|
export const TRANSPORT_PARTNER_ORG_TAG = 'transport-partner';
|
|
69
|
+
/** Value of LOCATION_PICKLIST_ID offered to every tenant: the vehicle's parking was not recorded. */
|
|
70
|
+
export const UNKNOWN_PARKING_ID = 'unknown';
|
|
69
71
|
|
|
70
72
|
//
|
|
71
73
|
// STATIC PICKLIST IDS FROM HyperDMS
|
package/src/types/crm.ts
CHANGED
|
@@ -240,18 +240,40 @@ export type CrmPublicSearchDefinitions = {
|
|
|
240
240
|
|
|
241
241
|
export enum MarketingChannel {
|
|
242
242
|
Email = 'email',
|
|
243
|
-
|
|
244
|
-
Sms = 'sms',
|
|
243
|
+
/** Calls, WhatsApp and SMS to a phone number. */
|
|
245
244
|
Phone = 'phone',
|
|
246
245
|
}
|
|
247
246
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
247
|
+
/** Why an address stopped receiving marketing. */
|
|
248
|
+
export enum MarketingOptOutReason {
|
|
249
|
+
/** A user switched the address off in the CRM. */
|
|
250
|
+
Human = 'human',
|
|
251
|
+
/** The recipient used the unsubscribe link in a campaign email. */
|
|
252
|
+
Unsubscribe = 'unsubscribe',
|
|
253
|
+
/** The recipient reported a campaign email as spam. */
|
|
254
|
+
Complaint = 'complaint',
|
|
255
|
+
/** SES Auto Validation dropped the address before sending because it expected a bounce. */
|
|
256
|
+
AutoValidation = 'auto-validation',
|
|
257
|
+
/** The recipient asked WhatsApp marketing to stop. */
|
|
258
|
+
WhatsappStop = 'whatsapp-stop',
|
|
259
|
+
}
|
|
252
260
|
|
|
253
|
-
|
|
254
|
-
|
|
261
|
+
/**
|
|
262
|
+
* An opted-out address in a tenant: an email address, or a phone number in E.164. The row exists
|
|
263
|
+
* only while the address is opted out; switching it back on deletes it.
|
|
264
|
+
*/
|
|
265
|
+
export type MarketingOptOut = {
|
|
266
|
+
channel: MarketingChannel;
|
|
267
|
+
address: string;
|
|
268
|
+
reason: MarketingOptOutReason;
|
|
269
|
+
updatedAt: string;
|
|
270
|
+
/**
|
|
271
|
+
* The user who switched it off, or `system` for the automatic reasons. Absent on opt-outs
|
|
272
|
+
* carried over from the old per-contact switches, which never recorded who set them.
|
|
273
|
+
*/
|
|
274
|
+
updatedBy?: string;
|
|
275
|
+
/** The campaign whose email led to it, for `unsubscribe`, `complaint` and `auto-validation`. */
|
|
276
|
+
campaignId?: string;
|
|
255
277
|
};
|
|
256
278
|
|
|
257
279
|
export enum PartnerLevel {
|
|
@@ -279,7 +301,6 @@ type BaseContact = {
|
|
|
279
301
|
organizationId?: string;
|
|
280
302
|
/** will not be filled if organizationId is present */
|
|
281
303
|
orgName?: string;
|
|
282
|
-
marketingPreferences?: MarketingPreferences;
|
|
283
304
|
clientLevel?: PartnerLevel;
|
|
284
305
|
|
|
285
306
|
disabledAt?: string;
|
package/src/types/deliveries.ts
CHANGED
|
@@ -6,11 +6,11 @@ export const PARK_DELIVERY_TYPE = 'park';
|
|
|
6
6
|
/** Supplier collection keeps the existing address value in stored transports. */
|
|
7
7
|
export enum PurchaseDeliveryTypes {
|
|
8
8
|
ADDRESS = 'address',
|
|
9
|
-
DROP_OFF =
|
|
9
|
+
DROP_OFF = 'park',
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export enum SalesDeliveryTypes {
|
|
13
|
-
PICK_UP =
|
|
13
|
+
PICK_UP = 'park',
|
|
14
14
|
ADDRESS = 'address',
|
|
15
15
|
PORT = 'port',
|
|
16
16
|
PORT_AND_SHIP = 'portAndShip',
|
|
@@ -79,6 +79,19 @@ export const debtorsDossierStatusToDateFieldMap: Record<
|
|
|
79
79
|
* Auto Follow-Up Types
|
|
80
80
|
******/
|
|
81
81
|
|
|
82
|
+
export type ReminderWhatsappAttemptStatus = 'SENT' | 'DELIVERED' | 'FAILED' | 'SKIPPED';
|
|
83
|
+
|
|
84
|
+
// see docs/adr/0026-whatsapp-leasing-notifications-alongside-email.md
|
|
85
|
+
export type ReminderWhatsappAttempt = {
|
|
86
|
+
dedupId: string;
|
|
87
|
+
templateId: string;
|
|
88
|
+
phone: string;
|
|
89
|
+
riskLevel: number;
|
|
90
|
+
sentAt: string;
|
|
91
|
+
status: ReminderWhatsappAttemptStatus;
|
|
92
|
+
failureReason?: string;
|
|
93
|
+
};
|
|
94
|
+
|
|
82
95
|
export type DebtorAutoFollowUp = {
|
|
83
96
|
id: string;
|
|
84
97
|
organizationId: string;
|
|
@@ -86,6 +99,7 @@ export type DebtorAutoFollowUp = {
|
|
|
86
99
|
status: 'ONGOING' | 'ERROR' | 'SUCCESS' | 'EXPIRED' | 'DISCARDED';
|
|
87
100
|
discardReason?: string;
|
|
88
101
|
emails: EmailMessage[];
|
|
102
|
+
whatsapps?: ReminderWhatsappAttempt[];
|
|
89
103
|
createdAt: string;
|
|
90
104
|
modifiedAt: string;
|
|
91
105
|
completedAt?: string;
|
package/src/types/tenants.ts
CHANGED
|
@@ -60,6 +60,12 @@ export enum TenantModule {
|
|
|
60
60
|
TrackAndTrace = 'track_and_trace',
|
|
61
61
|
Bootstrap = 'bootstrap',
|
|
62
62
|
WhatsappCrisp = 'whatsapp_crisp',
|
|
63
|
+
/**
|
|
64
|
+
* The app sends leasing clients WhatsApp messages on its own: the monthly invoice summary and
|
|
65
|
+
* the overdue payment reminders (ADR-0026). Separate from `whatsapp_crisp`, which only lets
|
|
66
|
+
* users send WhatsApp messages themselves. Both flows also need the tenant's Crisp settings.
|
|
67
|
+
*/
|
|
68
|
+
WhatsappNotifications = 'whatsapp_notifications',
|
|
63
69
|
Hexon = 'hexon',
|
|
64
70
|
DeliveryConfirmation = 'delivery_confirmation',
|
|
65
71
|
Transport = 'transport',
|
|
@@ -150,6 +156,12 @@ export type Branch = {
|
|
|
150
156
|
standard: string; //percentage value (e.g. 21)
|
|
151
157
|
};
|
|
152
158
|
location: LocationValue;
|
|
159
|
+
/**
|
|
160
|
+
* The parking (a `location` picklist value) this branch's vehicles go to when their purchase
|
|
161
|
+
* names none. Vehicle entry offers it as the stock destination, and the transport ITR prints
|
|
162
|
+
* it as the park. Absent = the ITR prints the branch's own address.
|
|
163
|
+
*/
|
|
164
|
+
defaultParking?: string;
|
|
153
165
|
|
|
154
166
|
storecoveExternalId?: number;
|
|
155
167
|
// true if the branch is for a company where VAT is due when the invoice
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { MarketingChannel } from '@bisondesk/core-sdk/types/crm';
|
|
2
|
+
import { describe, expect, test } from 'vitest';
|
|
3
|
+
import {
|
|
4
|
+
getContactMarketingAddresses,
|
|
5
|
+
normalizeEmail,
|
|
6
|
+
normalizeMarketingAddress,
|
|
7
|
+
} from './marketing-opt-outs.js';
|
|
8
|
+
|
|
9
|
+
describe('normalizeMarketingAddress', () => {
|
|
10
|
+
test('trims and lowercases an email address', () => {
|
|
11
|
+
expect(normalizeEmail(' Buyer@Example.com ')).toBe('buyer@example.com');
|
|
12
|
+
expect(normalizeMarketingAddress(MarketingChannel.Email, ' Buyer@Example.com ')).toBe(
|
|
13
|
+
'buyer@example.com',
|
|
14
|
+
);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('keeps a phone number as its E.164 string', () => {
|
|
18
|
+
expect(normalizeMarketingAddress(MarketingChannel.Phone, ' +32470123456 ')).toBe(
|
|
19
|
+
'+32470123456',
|
|
20
|
+
);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('lists a contact’s email addresses and phone numbers per channel', () => {
|
|
25
|
+
expect(
|
|
26
|
+
getContactMarketingAddresses({
|
|
27
|
+
emails: ['buyer@example.com'],
|
|
28
|
+
phones: [{ e164: '+32470123456' }, { e164: '+32470999999' }],
|
|
29
|
+
}),
|
|
30
|
+
).toEqual({
|
|
31
|
+
[MarketingChannel.Email]: ['buyer@example.com'],
|
|
32
|
+
[MarketingChannel.Phone]: ['+32470123456', '+32470999999'],
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { MarketingChannel } from '../types/crm.js';
|
|
2
|
+
|
|
3
|
+
/** The form marketing email addresses are stored and compared in: trimmed and lowercased. */
|
|
4
|
+
export const normalizeEmail = (email: string): string => email.trim().toLowerCase();
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The form an opted-out address is stored and compared in: email addresses through
|
|
8
|
+
* `normalizeEmail`, phone numbers as the E.164 string contacts already store.
|
|
9
|
+
*/
|
|
10
|
+
export const normalizeMarketingAddress = (channel: MarketingChannel, address: string): string =>
|
|
11
|
+
channel === MarketingChannel.Email ? normalizeEmail(address) : address.trim();
|
|
12
|
+
|
|
13
|
+
/** A contact's addresses per marketing channel: its email addresses and its phone numbers. */
|
|
14
|
+
export const getContactMarketingAddresses = (contact: {
|
|
15
|
+
emails: string[];
|
|
16
|
+
phones: { e164: string }[];
|
|
17
|
+
}): Record<MarketingChannel, string[]> => ({
|
|
18
|
+
[MarketingChannel.Email]: contact.emails,
|
|
19
|
+
[MarketingChannel.Phone]: contact.phones.map((phone) => phone.e164),
|
|
20
|
+
});
|
|
@@ -15,8 +15,7 @@ const ORG_IDENTITY_EDIT_ROLES: AppRoles[] = [
|
|
|
15
15
|
* except ORG_IDENTITY_EDIT_ROLES — by then they are already printed on legal documents.
|
|
16
16
|
*
|
|
17
17
|
* Shared so the server guard and the form's disabled state read one rule. Fails open on a missing
|
|
18
|
-
* createdAt
|
|
19
|
-
* date must not be frozen out of its own first write.
|
|
18
|
+
* createdAt, so a new org stays editable in the form until its first save.
|
|
20
19
|
*/
|
|
21
20
|
export const isOrgIdentityLocked = (
|
|
22
21
|
createdAt: string | undefined,
|