From 6caf37254fed1dbf796c655b80ab005d5686ba96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20R=C3=B8svik?= Date: Thu, 2 Jul 2026 13:53:16 +0200 Subject: [PATCH 1/9] feat: add global message schema --- src/common/app-platform.ts | 4 +++ src/common/language-and-text.ts | 14 +++++++++ src/global-messages/index.ts | 5 +++ src/global-messages/types.ts | 55 +++++++++++++++++++++++++++++++++ src/index.ts | 4 +++ src/rules/index.ts | 1 + src/rules/types.ts | 28 +++++++++++++++++ 7 files changed, 111 insertions(+) create mode 100644 src/common/app-platform.ts create mode 100644 src/common/language-and-text.ts create mode 100644 src/global-messages/index.ts create mode 100644 src/global-messages/types.ts create mode 100644 src/rules/index.ts create mode 100644 src/rules/types.ts diff --git a/src/common/app-platform.ts b/src/common/app-platform.ts new file mode 100644 index 0000000..3e2f3e7 --- /dev/null +++ b/src/common/app-platform.ts @@ -0,0 +1,4 @@ +import {z} from 'zod'; + +export const AppPlatform = z.enum(['ios', 'android']); +export type AppPlatform = z.infer; diff --git a/src/common/language-and-text.ts b/src/common/language-and-text.ts new file mode 100644 index 0000000..95e675b --- /dev/null +++ b/src/common/language-and-text.ts @@ -0,0 +1,14 @@ +import {z} from 'zod'; + +export const LanguageAndTextSchema = z.union([ + z.object({ + lang: z.string(), + value: z.string(), + }), + z.object({ + language: z.string().optional(), + value: z.string().optional(), + }), +]); + +export type LanguageAndTextType = z.infer; diff --git a/src/global-messages/index.ts b/src/global-messages/index.ts new file mode 100644 index 0000000..151f5a8 --- /dev/null +++ b/src/global-messages/index.ts @@ -0,0 +1,5 @@ +export { + GlobalMessageContextEnum, + GlobalMessageSchema, + GlobalMessageType, +} from './types'; diff --git a/src/global-messages/types.ts b/src/global-messages/types.ts new file mode 100644 index 0000000..45bd711 --- /dev/null +++ b/src/global-messages/types.ts @@ -0,0 +1,55 @@ +import {z} from 'zod'; +import {LanguageAndTextSchema} from '../common/language-and-text'; +import {Rule} from '../rules/types'; +import {AppPlatform} from '../common/app-platform'; + +export enum GlobalMessageContextEnum { + appAssistant = 'app-assistant', + appDepartures = 'app-departures', + appTicketing = 'app-ticketing', + appProfile = 'app-profile', + appPurchaseOverview = 'app-purchase-overview', + appPurchaseConfirmation = 'app-purchase-confirmation', + appPurchaseConfirmationBottom = 'app-purchase-confirmation-bottom', + appFareContractDetails = 'app-fare-contract-details', + appDepartureDetails = 'app-departure-details', + appTripDetails = 'app-trip-details', + appTripResults = 'app-trip-results', + appServiceDisruptions = 'app-service-disruptions', + appLogin = 'app-login', + appLoginPhone = 'app-login-phone', + appPointsScreen = 'app-points-screen', + + plannerWeb = 'planner-web', + plannerWebDepartures = 'planner-web-departures', + plannerWebDeparturesDetails = 'planner-web-departures-details', + plannerWebTrip = 'planner-web-trip', + plannerWebDetails = 'planner-web-details', + + webTicketing = 'web-ticketing', + webOverview = 'web-overview', + webPayment = 'web-payment', + webLogin = 'web-login', + webLoginPhone = 'web-login-phone', + webLoginEmail = 'web-login-email', +} + +export const GlobalMessageSchema = z.object({ + id: z.string(), + active: z.boolean(), + title: z.array(LanguageAndTextSchema).optional(), + body: z.array(LanguageAndTextSchema), + link: z.array(LanguageAndTextSchema).optional(), + linkText: z.array(LanguageAndTextSchema).optional(), + type: z.enum(['error', 'valid', 'info', 'warning']), + subtle: z.boolean().optional(), + context: z.array(z.nativeEnum(GlobalMessageContextEnum)), + isDismissable: z.boolean().optional(), + appPlatforms: z.array(AppPlatform).optional(), + appVersionMin: z.string().optional(), + appVersionMax: z.string().optional(), + startDate: z.number().optional(), + endDate: z.number().optional(), + rules: z.array(Rule).optional(), +}); +export type GlobalMessageType = z.infer; diff --git a/src/index.ts b/src/index.ts index 3f8756c..44675f5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,9 @@ export * from './fare-contract'; export * from './offers/ticket-offer'; +export * from './rules'; +export * from './global-messages'; +export * from './common/language-and-text'; +export * from './common/app-platform'; export {ErrorResponse, HttpError} from './error-response'; export {BookingAvailabilityType} from './offers/booking'; export {formatNumberToString} from './utils'; diff --git a/src/rules/index.ts b/src/rules/index.ts new file mode 100644 index 0000000..7ba4af3 --- /dev/null +++ b/src/rules/index.ts @@ -0,0 +1 @@ +export {Rule, RuleOperator, RuleVariables} from './types'; diff --git a/src/rules/types.ts b/src/rules/types.ts new file mode 100644 index 0000000..fe87e03 --- /dev/null +++ b/src/rules/types.ts @@ -0,0 +1,28 @@ +import {z} from 'zod'; + +const RuleValue = z.union([z.string(), z.number(), z.boolean(), z.null()]); +type RuleValue = z.infer; + +export type RuleVariables = { + [key: string]: RuleValue | RuleValue[]; +}; + +export enum RuleOperator { + equalTo = 'equalTo', + notEqualTo = 'notEqualTo', + greaterThan = 'greaterThan', + lessThan = 'lessThan', + greaterThanOrEqualTo = 'greaterThanOrEqualTo', + lessThanOrEqualTo = 'lessThanOrEqualTo', + contains = 'contains', + notContains = 'notContains', + onlyContains = 'onlyContains', +} + +export const Rule = z.object({ + variable: z.string().describe('key of RuleVariables'), // if passing down ruleVariables to the zod parsing, this can be enforced runtime with refine + operator: z.nativeEnum(RuleOperator), + value: RuleValue, + groupId: z.string().optional(), +}); +export type Rule = z.infer; From 4fabe463ad259efc5b3771d95acb1ce87811c165 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20R=C3=B8svik?= Date: Thu, 2 Jul 2026 14:17:47 +0200 Subject: [PATCH 2/9] feat: add checkRules --- src/rules/check.ts | 100 +++++++++++++++++++++++++++++++++++++++++++++ src/rules/index.ts | 1 + 2 files changed, 101 insertions(+) create mode 100644 src/rules/check.ts diff --git a/src/rules/check.ts b/src/rules/check.ts new file mode 100644 index 0000000..9984597 --- /dev/null +++ b/src/rules/check.ts @@ -0,0 +1,100 @@ +import {Rule, RuleOperator, RuleVariables} from './types'; + +export const checkRules = ( + rules: Rule[], + localVariables: RuleVariables, +): boolean => { + // Verify that all ungrouped rules pass + const ungroupedRules = rules.filter((rule) => rule.groupId === undefined); + const allOfUngrouped = ungroupedRules.every((rule) => + checkRule(rule, localVariables), + ); + if (!allOfUngrouped) return false; + + // Verify that groups pass one of their rules + const uniqueGroups = new Set(); + rules.forEach((rule) => { + if (rule.groupId) uniqueGroups.add(rule.groupId); + }); + for (const groupId of uniqueGroups) { + const groupRules = rules.filter((rule) => rule.groupId === groupId); + const oneOfRuleInGroup = groupRules.find((rule) => + checkRule(rule, localVariables), + ); + if (!oneOfRuleInGroup) return false; + } + return true; +}; + +const checkRule = ( + globalMessageRule: Rule, + localVariables: RuleVariables, +): boolean => { + const { + operator, + value: ruleValue, + variable: variableName, + } = globalMessageRule; + if (!(variableName in localVariables)) return false; + const localValue = localVariables[variableName]; + switch (operator) { + case RuleOperator.equalTo: + if ( + ['string', 'number', 'boolean'].includes(typeof localValue) || + localValue === null + ) + return localValue === ruleValue; + return false; + case RuleOperator.notEqualTo: + if ( + ['string', 'number', 'boolean'].includes(typeof localValue) || + localValue === null + ) + return localValue !== ruleValue; + return false; + case RuleOperator.greaterThan: + if ( + ['string', 'number'].includes(typeof localValue) && + ['string', 'number'].includes(typeof ruleValue) + ) + return (localValue as string | number) > (ruleValue as string | number); + return false; + case RuleOperator.lessThan: + if ( + ['string', 'number'].includes(typeof localValue) && + ['string', 'number'].includes(typeof ruleValue) + ) + return (localValue as string | number) < (ruleValue as string | number); + return false; + case RuleOperator.greaterThanOrEqualTo: + if ( + ['string', 'number'].includes(typeof localValue) && + ['string', 'number'].includes(typeof ruleValue) + ) + return ( + (localValue as string | number) >= (ruleValue as string | number) + ); + return false; + case RuleOperator.lessThanOrEqualTo: + if ( + ['string', 'number'].includes(typeof localValue) && + ['string', 'number'].includes(typeof ruleValue) + ) + return ( + (localValue as string | number) <= (ruleValue as string | number) + ); + return false; + case RuleOperator.contains: + if (Array.isArray(localValue)) return localValue.includes(ruleValue); + return false; + case RuleOperator.notContains: + if (Array.isArray(localValue)) return !localValue.includes(ruleValue); + return false; + case RuleOperator.onlyContains: + if (Array.isArray(localValue)) + return localValue.every((v) => v === ruleValue); + return false; + default: + return false; + } +}; diff --git a/src/rules/index.ts b/src/rules/index.ts index 7ba4af3..3619b42 100644 --- a/src/rules/index.ts +++ b/src/rules/index.ts @@ -1 +1,2 @@ export {Rule, RuleOperator, RuleVariables} from './types'; +export {checkRules} from './check'; From 58cf6d95cdf16735c2d5ba0db88b382e93a9d29b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20R=C3=B8svik?= Date: Thu, 2 Jul 2026 14:23:53 +0200 Subject: [PATCH 3/9] chore: add jest types to tsconfig --- tsconfig.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index 135776d..ce8bde3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,8 @@ "compilerOptions": { "rootDir": "src", "outDir": "lib", - "declaration": true + "declaration": true, + "types": ["jest"] }, "include": ["src"], "exclude": ["node_modules"] From 3f415a99ec7d33b433eca9da96aa93dcfdf8c1f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20R=C3=B8svik?= Date: Thu, 2 Jul 2026 14:35:15 +0200 Subject: [PATCH 4/9] test: add tests for rules --- src/rules/__tests__/rule-grouping.test.ts | 83 +++++++++ src/rules/__tests__/rule-operators.test.ts | 194 +++++++++++++++++++++ src/rules/check.ts | 2 +- 3 files changed, 278 insertions(+), 1 deletion(-) create mode 100644 src/rules/__tests__/rule-grouping.test.ts create mode 100644 src/rules/__tests__/rule-operators.test.ts diff --git a/src/rules/__tests__/rule-grouping.test.ts b/src/rules/__tests__/rule-grouping.test.ts new file mode 100644 index 0000000..6618995 --- /dev/null +++ b/src/rules/__tests__/rule-grouping.test.ts @@ -0,0 +1,83 @@ +import {checkRules} from '../check'; +import {Rule, RuleOperator, RuleVariables} from '../types'; + +const createRule = ( + variable: string, + operator: RuleOperator, + value: Rule['value'], + groupId?: string, +): Rule => ({variable, operator, value, groupId}); + +describe('checkRules without groups', () => { + it('returns true when there are no rules', () => { + const localVariables: RuleVariables = {}; + expect(checkRules([], localVariables)).toEqual(true); + }); + + it('returns true when all ungrouped rules pass', () => { + const localVariables: RuleVariables = {name: 'foo', age: 10}; + const rules = [ + createRule('name', RuleOperator.equalTo, 'foo'), + createRule('age', RuleOperator.greaterThan, 5), + ]; + expect(checkRules(rules, localVariables)).toEqual(true); + }); + + it('returns false when one ungrouped rule fails', () => { + const localVariables: RuleVariables = {name: 'foo', age: 10}; + const rules = [ + createRule('name', RuleOperator.equalTo, 'foo'), + createRule('age', RuleOperator.lessThan, 5), + ]; + expect(checkRules(rules, localVariables)).toEqual(false); + }); + + it('returns false when all ungrouped rules fail', () => { + const localVariables: RuleVariables = {name: 'foo'}; + const rules = [createRule('name', RuleOperator.equalTo, 'not-foo')]; + expect(checkRules(rules, localVariables)).toEqual(false); + }); +}); + +describe('checkRules with a single group', () => { + it('returns true when one rule in the group passes', () => { + const localVariables: RuleVariables = {name: 'foo'}; + const rules = [ + createRule('name', RuleOperator.equalTo, 'foo', 'group1'), + createRule('name', RuleOperator.equalTo, 'bar', 'group1'), + ]; + expect(checkRules(rules, localVariables)).toEqual(true); + }); + + it('returns false when no rule in the group passes', () => { + const localVariables: RuleVariables = {name: 'foo'}; + const rules = [ + createRule('name', RuleOperator.equalTo, 'bar', 'group1'), + createRule('name', RuleOperator.equalTo, 'baz', 'group1'), + ]; + expect(checkRules(rules, localVariables)).toEqual(false); + }); +}); + +describe('checkRules with multiple groups', () => { + it('returns true when each group has at least one passing rule', () => { + const localVariables: RuleVariables = {name: 'foo', age: 10}; + const rules = [ + createRule('name', RuleOperator.equalTo, 'foo', 'group1'), + createRule('name', RuleOperator.equalTo, 'bar', 'group1'), + createRule('age', RuleOperator.lessThan, 5, 'group2'), + createRule('age', RuleOperator.greaterThan, 5, 'group2'), + ]; + expect(checkRules(rules, localVariables)).toEqual(true); + }); + + it('returns false when one group has no passing rule', () => { + const localVariables: RuleVariables = {name: 'foo', age: 10}; + const rules = [ + createRule('name', RuleOperator.equalTo, 'foo', 'group1'), + createRule('age', RuleOperator.lessThan, 5, 'group2'), + createRule('age', RuleOperator.equalTo, 999, 'group2'), + ]; + expect(checkRules(rules, localVariables)).toEqual(false); + }); +}); diff --git a/src/rules/__tests__/rule-operators.test.ts b/src/rules/__tests__/rule-operators.test.ts new file mode 100644 index 0000000..1515670 --- /dev/null +++ b/src/rules/__tests__/rule-operators.test.ts @@ -0,0 +1,194 @@ +import {checkRule} from '../check'; +import {Rule, RuleOperator, RuleVariables} from '../types'; + +const createRule = ( + variable: string, + operator: RuleOperator, + value: Rule['value'], +): Rule => ({variable, operator, value}); + +describe('RuleOperator.equalTo', () => { + it('returns true when string values are equal', () => { + const localVariables: RuleVariables = {name: 'foo'}; + const rule = createRule('name', RuleOperator.equalTo, 'foo'); + expect(checkRule(rule, localVariables)).toEqual(true); + }); + + it('returns true when number values are equal', () => { + const localVariables: RuleVariables = {age: 5}; + const rule = createRule('age', RuleOperator.equalTo, 5); + expect(checkRule(rule, localVariables)).toEqual(true); + }); + + it('returns false when values are not equal', () => { + const localVariables: RuleVariables = {name: 'foo'}; + const rule = createRule('name', RuleOperator.equalTo, 'not-foo'); + expect(checkRule(rule, localVariables)).toEqual(false); + }); + + it('returns false when number values are not equal', () => { + const localVariables: RuleVariables = {age: 5}; + const rule = createRule('age', RuleOperator.equalTo, 10); + expect(checkRule(rule, localVariables)).toEqual(false); + }); +}); + +describe('RuleOperator.notEqualTo', () => { + it('returns true when values are not equal', () => { + const localVariables: RuleVariables = {name: 'foo'}; + const rule = createRule('name', RuleOperator.notEqualTo, 'not-foo'); + expect(checkRule(rule, localVariables)).toEqual(true); + }); + + it('returns false when values are equal', () => { + const localVariables: RuleVariables = {age: 5}; + const rule = createRule('age', RuleOperator.notEqualTo, 5); + expect(checkRule(rule, localVariables)).toEqual(false); + }); +}); + +describe('RuleOperator.greaterThan', () => { + it('returns true when local number is greater than rule number', () => { + const localVariables: RuleVariables = {age: 10}; + const rule = createRule('age', RuleOperator.greaterThan, 5); + expect(checkRule(rule, localVariables)).toEqual(true); + }); +}); + +describe('RuleOperator.lessThan', () => { + it('returns true when local number is less than rule number', () => { + const localVariables: RuleVariables = {age: 5}; + const rule = createRule('age', RuleOperator.lessThan, 10); + expect(checkRule(rule, localVariables)).toEqual(true); + }); +}); + +describe('RuleOperator.greaterThanOrEqualTo', () => { + it('returns true when local number is greater than rule number', () => { + const localVariables: RuleVariables = {age: 10}; + const rule = createRule('age', RuleOperator.greaterThanOrEqualTo, 5); + expect(checkRule(rule, localVariables)).toEqual(true); + }); + + it('returns true when local number equals rule number', () => { + const localVariables: RuleVariables = {age: 5}; + const rule = createRule('age', RuleOperator.greaterThanOrEqualTo, 5); + expect(checkRule(rule, localVariables)).toEqual(true); + }); + + it('returns false when local number is less than rule number', () => { + const localVariables: RuleVariables = {age: 3}; + const rule = createRule('age', RuleOperator.greaterThanOrEqualTo, 5); + expect(checkRule(rule, localVariables)).toEqual(false); + }); + + it('returns false when values are not comparable types', () => { + const localVariables: RuleVariables = {isActive: true}; + const rule = createRule( + 'isActive', + RuleOperator.greaterThanOrEqualTo, + true, + ); + expect(checkRule(rule, localVariables)).toEqual(false); + }); +}); + +describe('RuleOperator.lessThanOrEqualTo', () => { + it('returns true when local number is less than rule number', () => { + const localVariables: RuleVariables = {age: 3}; + const rule = createRule('age', RuleOperator.lessThanOrEqualTo, 5); + expect(checkRule(rule, localVariables)).toEqual(true); + }); + + it('returns true when local number equals rule number', () => { + const localVariables: RuleVariables = {age: 5}; + const rule = createRule('age', RuleOperator.lessThanOrEqualTo, 5); + expect(checkRule(rule, localVariables)).toEqual(true); + }); + + it('returns false when local number is greater than rule number', () => { + const localVariables: RuleVariables = {age: 10}; + const rule = createRule('age', RuleOperator.lessThanOrEqualTo, 5); + expect(checkRule(rule, localVariables)).toEqual(false); + }); + + it('returns false when values are not comparable types', () => { + const localVariables: RuleVariables = {isActive: true}; + const rule = createRule('isActive', RuleOperator.lessThanOrEqualTo, true); + expect(checkRule(rule, localVariables)).toEqual(false); + }); +}); + +describe('RuleOperator.contains', () => { + it('returns true when local array contains the rule value', () => { + const localVariables: RuleVariables = {tags: ['a', 'b', 'c']}; + const rule = createRule('tags', RuleOperator.contains, 'b'); + expect(checkRule(rule, localVariables)).toEqual(true); + }); + + it('returns false when local array does not contain the rule value', () => { + const localVariables: RuleVariables = {tags: ['a', 'b', 'c']}; + const rule = createRule('tags', RuleOperator.contains, 'd'); + expect(checkRule(rule, localVariables)).toEqual(false); + }); + + it('returns false when local value is not an array', () => { + const localVariables: RuleVariables = {tags: 'a'}; + const rule = createRule('tags', RuleOperator.contains, 'a'); + expect(checkRule(rule, localVariables)).toEqual(false); + }); +}); + +describe('RuleOperator.notContains', () => { + it('returns true when local array does not contain the rule value', () => { + const localVariables: RuleVariables = {tags: ['a', 'b', 'c']}; + const rule = createRule('tags', RuleOperator.notContains, 'd'); + expect(checkRule(rule, localVariables)).toEqual(true); + }); + + it('returns false when local array contains the rule value', () => { + const localVariables: RuleVariables = {tags: ['a', 'b', 'c']}; + const rule = createRule('tags', RuleOperator.notContains, 'b'); + expect(checkRule(rule, localVariables)).toEqual(false); + }); + + it('returns false when local value is not an array', () => { + const localVariables: RuleVariables = {tags: 'a'}; + const rule = createRule('tags', RuleOperator.notContains, 'd'); + expect(checkRule(rule, localVariables)).toEqual(false); + }); +}); + +describe('RuleOperator.onlyContains', () => { + it('returns true when every element in the local array equals the rule value', () => { + const localVariables: RuleVariables = {tags: ['a', 'a', 'a']}; + const rule = createRule('tags', RuleOperator.onlyContains, 'a'); + expect(checkRule(rule, localVariables)).toEqual(true); + }); + + it('returns true when the local array is empty', () => { + const localVariables: RuleVariables = {tags: []}; + const rule = createRule('tags', RuleOperator.onlyContains, 'a'); + expect(checkRule(rule, localVariables)).toEqual(true); + }); + + it('returns false when some element in the local array does not equal the rule value', () => { + const localVariables: RuleVariables = {tags: ['a', 'b', 'a']}; + const rule = createRule('tags', RuleOperator.onlyContains, 'a'); + expect(checkRule(rule, localVariables)).toEqual(false); + }); + + it('returns false when local value is not an array', () => { + const localVariables: RuleVariables = {tags: 'a'}; + const rule = createRule('tags', RuleOperator.onlyContains, 'a'); + expect(checkRule(rule, localVariables)).toEqual(false); + }); +}); + +describe('checkRule variable resolution', () => { + it('returns false when the variable is not present in localVariables', () => { + const localVariables: RuleVariables = {name: 'foo'}; + const rule = createRule('missing', RuleOperator.equalTo, 'foo'); + expect(checkRule(rule, localVariables)).toEqual(false); + }); +}); diff --git a/src/rules/check.ts b/src/rules/check.ts index 9984597..fd835de 100644 --- a/src/rules/check.ts +++ b/src/rules/check.ts @@ -26,7 +26,7 @@ export const checkRules = ( return true; }; -const checkRule = ( +export const checkRule = ( globalMessageRule: Rule, localVariables: RuleVariables, ): boolean => { From 891450a73576d19528aa486a1c60f13eb26f7b7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20R=C3=B8svik?= Date: Thu, 2 Jul 2026 14:36:37 +0200 Subject: [PATCH 5/9] chore: rename --- src/rules/__tests__/rule-grouping.test.ts | 2 +- src/rules/__tests__/rule-operators.test.ts | 2 +- src/rules/{check.ts => check-rules.ts} | 0 src/rules/index.ts | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename src/rules/{check.ts => check-rules.ts} (100%) diff --git a/src/rules/__tests__/rule-grouping.test.ts b/src/rules/__tests__/rule-grouping.test.ts index 6618995..d0cd250 100644 --- a/src/rules/__tests__/rule-grouping.test.ts +++ b/src/rules/__tests__/rule-grouping.test.ts @@ -1,4 +1,4 @@ -import {checkRules} from '../check'; +import {checkRules} from '../check-rules'; import {Rule, RuleOperator, RuleVariables} from '../types'; const createRule = ( diff --git a/src/rules/__tests__/rule-operators.test.ts b/src/rules/__tests__/rule-operators.test.ts index 1515670..e47669c 100644 --- a/src/rules/__tests__/rule-operators.test.ts +++ b/src/rules/__tests__/rule-operators.test.ts @@ -1,4 +1,4 @@ -import {checkRule} from '../check'; +import {checkRule} from '../check-rules'; import {Rule, RuleOperator, RuleVariables} from '../types'; const createRule = ( diff --git a/src/rules/check.ts b/src/rules/check-rules.ts similarity index 100% rename from src/rules/check.ts rename to src/rules/check-rules.ts diff --git a/src/rules/index.ts b/src/rules/index.ts index 3619b42..74a4328 100644 --- a/src/rules/index.ts +++ b/src/rules/index.ts @@ -1,2 +1,2 @@ export {Rule, RuleOperator, RuleVariables} from './types'; -export {checkRules} from './check'; +export {checkRules} from './check-rules'; From dc56cdc1853d13935baaba8363e8c69dd72f99eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20R=C3=B8svik?= Date: Thu, 2 Jul 2026 16:33:53 +0200 Subject: [PATCH 6/9] chore: remove language-and-text export --- src/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 44675f5..3a3cb5f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,6 @@ export * from './fare-contract'; export * from './offers/ticket-offer'; export * from './rules'; export * from './global-messages'; -export * from './common/language-and-text'; export * from './common/app-platform'; export {ErrorResponse, HttpError} from './error-response'; export {BookingAvailabilityType} from './offers/booking'; From f0c6881085051a9829b5ff907c7b9ea01bdb5cfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20R=C3=B8svik?= Date: Fri, 3 Jul 2026 09:54:09 +0200 Subject: [PATCH 7/9] chore: set start and end date as Date objects --- src/global-messages/types.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/global-messages/types.ts b/src/global-messages/types.ts index 45bd711..d54e2a0 100644 --- a/src/global-messages/types.ts +++ b/src/global-messages/types.ts @@ -48,8 +48,8 @@ export const GlobalMessageSchema = z.object({ appPlatforms: z.array(AppPlatform).optional(), appVersionMin: z.string().optional(), appVersionMax: z.string().optional(), - startDate: z.number().optional(), - endDate: z.number().optional(), + startDate: z.coerce.date(), + endDate: z.coerce.date(), rules: z.array(Rule).optional(), }); export type GlobalMessageType = z.infer; From 264077274b581d934fb688f086c1ef6033d91bd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20R=C3=B8svik?= Date: Fri, 3 Jul 2026 13:32:49 +0200 Subject: [PATCH 8/9] refactor: make global message schema generic on context enum --- src/global-messages/index.ts | 6 +-- src/global-messages/types.ts | 84 ++++++++++++++++-------------------- 2 files changed, 37 insertions(+), 53 deletions(-) diff --git a/src/global-messages/index.ts b/src/global-messages/index.ts index 151f5a8..bf7daf2 100644 --- a/src/global-messages/index.ts +++ b/src/global-messages/index.ts @@ -1,5 +1 @@ -export { - GlobalMessageContextEnum, - GlobalMessageSchema, - GlobalMessageType, -} from './types'; +export {createGlobalMessageSchema, GenericGlobalMessageType} from './types'; diff --git a/src/global-messages/types.ts b/src/global-messages/types.ts index d54e2a0..3e2062d 100644 --- a/src/global-messages/types.ts +++ b/src/global-messages/types.ts @@ -3,53 +3,41 @@ import {LanguageAndTextSchema} from '../common/language-and-text'; import {Rule} from '../rules/types'; import {AppPlatform} from '../common/app-platform'; -export enum GlobalMessageContextEnum { - appAssistant = 'app-assistant', - appDepartures = 'app-departures', - appTicketing = 'app-ticketing', - appProfile = 'app-profile', - appPurchaseOverview = 'app-purchase-overview', - appPurchaseConfirmation = 'app-purchase-confirmation', - appPurchaseConfirmationBottom = 'app-purchase-confirmation-bottom', - appFareContractDetails = 'app-fare-contract-details', - appDepartureDetails = 'app-departure-details', - appTripDetails = 'app-trip-details', - appTripResults = 'app-trip-results', - appServiceDisruptions = 'app-service-disruptions', - appLogin = 'app-login', - appLoginPhone = 'app-login-phone', - appPointsScreen = 'app-points-screen', - - plannerWeb = 'planner-web', - plannerWebDepartures = 'planner-web-departures', - plannerWebDeparturesDetails = 'planner-web-departures-details', - plannerWebTrip = 'planner-web-trip', - plannerWebDetails = 'planner-web-details', - - webTicketing = 'web-ticketing', - webOverview = 'web-overview', - webPayment = 'web-payment', - webLogin = 'web-login', - webLoginPhone = 'web-login-phone', - webLoginEmail = 'web-login-email', +/** + * Create a global message schema with a consumer specific context enum. + * + * @example + * export enum GlobalMessageContextEnum { + * webOverview = 'web-overview', + * } + * const GlobalMessageContextSchema = z.enum(GlobalMessageContextEnum); + * export const GlobalMessageSchema = createGlobalMessageSchema( + * GlobalMessageContextSchema, + * ); + */ +export function createGlobalMessageSchema( + contextEnum: ContextEnum, +) { + return z.object({ + id: z.string(), + active: z.boolean(), + title: z.array(LanguageAndTextSchema).optional(), + body: z.array(LanguageAndTextSchema), + link: z.array(LanguageAndTextSchema).optional(), + linkText: z.array(LanguageAndTextSchema).optional(), + type: z.enum(['error', 'valid', 'info', 'warning']), + subtle: z.boolean().optional(), + context: z.array(contextEnum), + isDismissable: z.boolean().optional(), + appPlatforms: z.array(AppPlatform).optional(), + appVersionMin: z.string().optional(), + appVersionMax: z.string().optional(), + startDate: z.coerce.date(), + endDate: z.coerce.date(), + rules: z.array(Rule).optional(), + }); } -export const GlobalMessageSchema = z.object({ - id: z.string(), - active: z.boolean(), - title: z.array(LanguageAndTextSchema).optional(), - body: z.array(LanguageAndTextSchema), - link: z.array(LanguageAndTextSchema).optional(), - linkText: z.array(LanguageAndTextSchema).optional(), - type: z.enum(['error', 'valid', 'info', 'warning']), - subtle: z.boolean().optional(), - context: z.array(z.nativeEnum(GlobalMessageContextEnum)), - isDismissable: z.boolean().optional(), - appPlatforms: z.array(AppPlatform).optional(), - appVersionMin: z.string().optional(), - appVersionMax: z.string().optional(), - startDate: z.coerce.date(), - endDate: z.coerce.date(), - rules: z.array(Rule).optional(), -}); -export type GlobalMessageType = z.infer; +export type GenericGlobalMessageType = z.infer< + ReturnType> +>; From bd92d484235163b101d678bacf8034b1c54471a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20R=C3=B8svik?= Date: Fri, 3 Jul 2026 14:10:51 +0200 Subject: [PATCH 9/9] fix: make start and end date optional --- src/global-messages/types.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/global-messages/types.ts b/src/global-messages/types.ts index 3e2062d..8239c00 100644 --- a/src/global-messages/types.ts +++ b/src/global-messages/types.ts @@ -32,8 +32,8 @@ export function createGlobalMessageSchema( appPlatforms: z.array(AppPlatform).optional(), appVersionMin: z.string().optional(), appVersionMax: z.string().optional(), - startDate: z.coerce.date(), - endDate: z.coerce.date(), + startDate: z.coerce.date().optional(), + endDate: z.coerce.date().optional(), rules: z.array(Rule).optional(), }); }