@revolugo/common 7.12.1 → 7.13.0-rc.0
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/package.json +1 -1
- package/src/cancellation-policies.test.ts +160 -0
- package/src/cancellation-policies.ts +62 -0
- package/src/constants/insurance.ts +48 -0
- package/src/constants/legal.ts +3 -0
- package/src/currencies/utils.ts +1 -1
- package/src/icons/index.ts +1 -0
- package/src/schemas/payment-methods.ts +3 -6
- package/src/types/elements/payment-method.ts +4 -4
- package/src/types/insurance.ts +18 -1
package/package.json
CHANGED
|
@@ -5,6 +5,7 @@ import { describe, expect, test } from 'vitest'
|
|
|
5
5
|
import {
|
|
6
6
|
getCurrentPenaltyPercentage,
|
|
7
7
|
getPenaltyPercentage,
|
|
8
|
+
isFreelyCancellableUntilHoursBeforeCheckIn,
|
|
8
9
|
sanitizeCancellationPolicies,
|
|
9
10
|
} from './cancellation-policies.ts'
|
|
10
11
|
import { type Dayjs, dayjs } from './utils/dayjs.ts'
|
|
@@ -1044,3 +1045,162 @@ describe('getCurrentPenaltyPercentage', () => {
|
|
|
1044
1045
|
).to.equal(PP_4)
|
|
1045
1046
|
})
|
|
1046
1047
|
})
|
|
1048
|
+
|
|
1049
|
+
describe('isFreelyCancellableUntilHoursBeforeCheckIn', () => {
|
|
1050
|
+
// Check-in is at the hotel's local midnight; policy dates are anchored to it.
|
|
1051
|
+
const cancelTimezone = 'Europe/Paris'
|
|
1052
|
+
const checkInDate = '2025-09-18'
|
|
1053
|
+
const freeFrom = '2025-08-01T00:00:00Z'
|
|
1054
|
+
// Local midnight of check-in in Paris (UTC+2 in September) expressed in UTC.
|
|
1055
|
+
const checkInInstant = dayjs.tz(checkInDate, cancelTimezone)
|
|
1056
|
+
|
|
1057
|
+
function freeUntil(dateTo: string): ICancellationPolicy[] {
|
|
1058
|
+
return [{ dateFrom: freeFrom, dateTo, penaltyPercentage: 0 }]
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
test('offers Flex when there are no policies', () => {
|
|
1062
|
+
expect(
|
|
1063
|
+
isFreelyCancellableUntilHoursBeforeCheckIn({
|
|
1064
|
+
cancellationPolicies: [],
|
|
1065
|
+
checkInDate,
|
|
1066
|
+
hoursBeforeCheckIn: 48,
|
|
1067
|
+
timezone: cancelTimezone,
|
|
1068
|
+
}),
|
|
1069
|
+
).toBe(false)
|
|
1070
|
+
})
|
|
1071
|
+
|
|
1072
|
+
test('offers Flex when the booking is non-refundable from the start', () => {
|
|
1073
|
+
expect(
|
|
1074
|
+
isFreelyCancellableUntilHoursBeforeCheckIn({
|
|
1075
|
+
cancellationPolicies: [
|
|
1076
|
+
{ dateFrom: freeFrom, dateTo: checkInDate, penaltyPercentage: 100 },
|
|
1077
|
+
],
|
|
1078
|
+
checkInDate,
|
|
1079
|
+
hoursBeforeCheckIn: 48,
|
|
1080
|
+
timezone: cancelTimezone,
|
|
1081
|
+
}),
|
|
1082
|
+
).toBe(false)
|
|
1083
|
+
})
|
|
1084
|
+
|
|
1085
|
+
test('offers Flex when the free window stops short of the 48h mark', () => {
|
|
1086
|
+
// Free only until 7 days before check-in → Flex extends the free window.
|
|
1087
|
+
const dateTo = checkInInstant.subtract(7, 'days').format()
|
|
1088
|
+
expect(
|
|
1089
|
+
isFreelyCancellableUntilHoursBeforeCheckIn({
|
|
1090
|
+
cancellationPolicies: freeUntil(dateTo),
|
|
1091
|
+
checkInDate,
|
|
1092
|
+
hoursBeforeCheckIn: 48,
|
|
1093
|
+
timezone: cancelTimezone,
|
|
1094
|
+
}),
|
|
1095
|
+
).toBe(false)
|
|
1096
|
+
})
|
|
1097
|
+
|
|
1098
|
+
test('suppresses Flex when free continuously until exactly the 48h mark', () => {
|
|
1099
|
+
const dateTo = checkInInstant.subtract(48, 'hours').format()
|
|
1100
|
+
expect(
|
|
1101
|
+
isFreelyCancellableUntilHoursBeforeCheckIn({
|
|
1102
|
+
cancellationPolicies: freeUntil(dateTo),
|
|
1103
|
+
checkInDate,
|
|
1104
|
+
hoursBeforeCheckIn: 48,
|
|
1105
|
+
timezone: cancelTimezone,
|
|
1106
|
+
}),
|
|
1107
|
+
).toBe(true)
|
|
1108
|
+
})
|
|
1109
|
+
|
|
1110
|
+
test('suppresses Flex when the free window reaches closer to check-in than 48h', () => {
|
|
1111
|
+
const dateTo = checkInInstant.subtract(24, 'hours').format()
|
|
1112
|
+
expect(
|
|
1113
|
+
isFreelyCancellableUntilHoursBeforeCheckIn({
|
|
1114
|
+
cancellationPolicies: freeUntil(dateTo),
|
|
1115
|
+
checkInDate,
|
|
1116
|
+
hoursBeforeCheckIn: 48,
|
|
1117
|
+
timezone: cancelTimezone,
|
|
1118
|
+
}),
|
|
1119
|
+
).toBe(true)
|
|
1120
|
+
})
|
|
1121
|
+
|
|
1122
|
+
test('offers Flex when a penalty head start precedes the free window', () => {
|
|
1123
|
+
// 50% early then free in the middle: Flex refunds that early period in full,
|
|
1124
|
+
// so it is strictly better and must be offered.
|
|
1125
|
+
expect(
|
|
1126
|
+
isFreelyCancellableUntilHoursBeforeCheckIn({
|
|
1127
|
+
cancellationPolicies: [
|
|
1128
|
+
{
|
|
1129
|
+
dateFrom: '2026-06-04T00:00:00+00:00',
|
|
1130
|
+
dateTo: '2026-06-15T00:00:00+00:00',
|
|
1131
|
+
penaltyPercentage: 50,
|
|
1132
|
+
},
|
|
1133
|
+
{
|
|
1134
|
+
dateFrom: '2026-06-15T00:00:00+00:00',
|
|
1135
|
+
dateTo: '2026-06-16T00:00:00+00:00',
|
|
1136
|
+
penaltyPercentage: 0,
|
|
1137
|
+
},
|
|
1138
|
+
{
|
|
1139
|
+
dateFrom: '2026-06-16T00:00:00+00:00',
|
|
1140
|
+
dateTo: '2026-06-18T00:00:00+00:00',
|
|
1141
|
+
penaltyPercentage: 100,
|
|
1142
|
+
},
|
|
1143
|
+
],
|
|
1144
|
+
checkInDate: '2026-06-18',
|
|
1145
|
+
hoursBeforeCheckIn: 48,
|
|
1146
|
+
timezone: 'UTC',
|
|
1147
|
+
}),
|
|
1148
|
+
).toBe(false)
|
|
1149
|
+
})
|
|
1150
|
+
|
|
1151
|
+
test('suppresses Flex across two contiguous free windows reaching the 48h mark', () => {
|
|
1152
|
+
expect(
|
|
1153
|
+
isFreelyCancellableUntilHoursBeforeCheckIn({
|
|
1154
|
+
cancellationPolicies: [
|
|
1155
|
+
{
|
|
1156
|
+
dateFrom: '2026-06-04T00:00:00+00:00',
|
|
1157
|
+
dateTo: '2026-06-10T00:00:00+00:00',
|
|
1158
|
+
penaltyPercentage: 0,
|
|
1159
|
+
},
|
|
1160
|
+
{
|
|
1161
|
+
dateFrom: '2026-06-10T00:00:00+00:00',
|
|
1162
|
+
dateTo: '2026-06-16T00:00:00+00:00',
|
|
1163
|
+
penaltyPercentage: 0,
|
|
1164
|
+
},
|
|
1165
|
+
{
|
|
1166
|
+
dateFrom: '2026-06-16T00:00:00+00:00',
|
|
1167
|
+
dateTo: '2026-06-18T00:00:00+00:00',
|
|
1168
|
+
penaltyPercentage: 100,
|
|
1169
|
+
},
|
|
1170
|
+
],
|
|
1171
|
+
checkInDate: '2026-06-18',
|
|
1172
|
+
hoursBeforeCheckIn: 48,
|
|
1173
|
+
timezone: 'UTC',
|
|
1174
|
+
}),
|
|
1175
|
+
).toBe(true)
|
|
1176
|
+
})
|
|
1177
|
+
|
|
1178
|
+
test('suppresses Flex for a UTC+2 policy free until local midnight 48h before check-in', () => {
|
|
1179
|
+
// Real-world data: free window ends at 22:00Z = local midnight (UTC+2),
|
|
1180
|
+
// exactly 48h before a check-in stored as a bare date.
|
|
1181
|
+
expect(
|
|
1182
|
+
isFreelyCancellableUntilHoursBeforeCheckIn({
|
|
1183
|
+
cancellationPolicies: [
|
|
1184
|
+
{
|
|
1185
|
+
dateFrom: '2026-06-04T12:47:07+00:00',
|
|
1186
|
+
dateTo: '2026-06-15T22:00:00+00:00',
|
|
1187
|
+
penaltyPercentage: 0,
|
|
1188
|
+
},
|
|
1189
|
+
{
|
|
1190
|
+
dateFrom: '2026-06-15T22:00:00+00:00',
|
|
1191
|
+
dateTo: '2026-06-16T22:00:00+00:00',
|
|
1192
|
+
penaltyPercentage: 50,
|
|
1193
|
+
},
|
|
1194
|
+
{
|
|
1195
|
+
dateFrom: '2026-06-16T22:00:00+00:00',
|
|
1196
|
+
dateTo: '2026-06-17T22:00:00+00:00',
|
|
1197
|
+
penaltyPercentage: 100,
|
|
1198
|
+
},
|
|
1199
|
+
],
|
|
1200
|
+
checkInDate: '2026-06-18',
|
|
1201
|
+
hoursBeforeCheckIn: 48,
|
|
1202
|
+
timezone: cancelTimezone,
|
|
1203
|
+
}),
|
|
1204
|
+
).toBe(true)
|
|
1205
|
+
})
|
|
1206
|
+
})
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// oxlint-disable max-lines
|
|
1
2
|
import { compact } from './utils/compact.ts'
|
|
2
3
|
import { type Dayjs, dayjs } from './utils/dayjs.ts'
|
|
3
4
|
import { isEmpty } from './utils/is-empty.ts'
|
|
@@ -523,6 +524,67 @@ export function adjustForCustomers(
|
|
|
523
524
|
}))
|
|
524
525
|
}
|
|
525
526
|
|
|
527
|
+
/**
|
|
528
|
+
* Whether the native cancellation policy is at least as good as Revolugo Flex
|
|
529
|
+
* (Meetch): the booking can be cancelled for free continuously from booking time
|
|
530
|
+
* until `hoursBeforeCheckIn` before check-in. When it can, Flex — which only
|
|
531
|
+
* guarantees a full refund up to that same window — brings no extra benefit and
|
|
532
|
+
* must not be offered as an addon.
|
|
533
|
+
*
|
|
534
|
+
* Flex IS better (and must be offered) as soon as the native policy charges any
|
|
535
|
+
* penalty inside that window — e.g. a non-refundable/penalty head start before a
|
|
536
|
+
* free window, a gap in coverage, or a free window that stops short of the 48h
|
|
537
|
+
* mark.
|
|
538
|
+
*
|
|
539
|
+
* `checkInDate` is interpreted at the start of day in `timezone` so the
|
|
540
|
+
* threshold shares the same time base as the policy dates (which are anchored to
|
|
541
|
+
* the hotel's local midnight). Without this, a date-only check-in parsed as UTC
|
|
542
|
+
* drifts by the hotel's UTC offset and the comparison misses by a few hours.
|
|
543
|
+
*/
|
|
544
|
+
export function isFreelyCancellableUntilHoursBeforeCheckIn({
|
|
545
|
+
cancellationPolicies,
|
|
546
|
+
checkInDate,
|
|
547
|
+
hoursBeforeCheckIn,
|
|
548
|
+
timezone,
|
|
549
|
+
}: {
|
|
550
|
+
cancellationPolicies: ICancellationPolicy[]
|
|
551
|
+
checkInDate: string
|
|
552
|
+
hoursBeforeCheckIn: number
|
|
553
|
+
timezone: string
|
|
554
|
+
}): boolean {
|
|
555
|
+
const threshold = dayjs
|
|
556
|
+
.tz(checkInDate, timezone)
|
|
557
|
+
.subtract(hoursBeforeCheckIn, 'hours')
|
|
558
|
+
|
|
559
|
+
const sanitizedPolicies = getSanitizedCancellationPolicies({
|
|
560
|
+
cancellationPolicies,
|
|
561
|
+
checkInDate,
|
|
562
|
+
timezone,
|
|
563
|
+
})
|
|
564
|
+
|
|
565
|
+
// Walk the policies from booking time, following an unbroken chain of free
|
|
566
|
+
// (0% penalty) windows. The first penalty or gap stops the chain.
|
|
567
|
+
let freeUntil: Dayjs | null = null
|
|
568
|
+
for (const policy of sanitizedPolicies) {
|
|
569
|
+
if (policy.penaltyPercentage !== 0) {
|
|
570
|
+
break
|
|
571
|
+
}
|
|
572
|
+
if (freeUntil && dayjs(policy.dateFrom).isAfter(freeUntil)) {
|
|
573
|
+
break
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const dateTo = dayjs(policy.dateTo)
|
|
577
|
+
if (!freeUntil || dateTo.isAfter(freeUntil)) {
|
|
578
|
+
freeUntil = dateTo
|
|
579
|
+
}
|
|
580
|
+
if (freeUntil.isSameOrAfter(threshold)) {
|
|
581
|
+
return true
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
return false
|
|
586
|
+
}
|
|
587
|
+
|
|
526
588
|
export function isFreeCancellable(cps: ICancellationPolicy[]): boolean {
|
|
527
589
|
const freeCp = cps.find(cp => cp.penaltyPercentage === 0)
|
|
528
590
|
return (
|
|
@@ -1,2 +1,50 @@
|
|
|
1
1
|
export const MEETCH_INSURANCE_TYPE_NAME = 'Meetch Insurance'
|
|
2
2
|
export const MEETCH_INSURANCE_TYPE_SLUG = 'meetch-insurance'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Single source of truth for the Revolugo Flex (Meetch) cancellation window:
|
|
6
|
+
* hours before check-in until which a claim is eligible. Consumed both by the
|
|
7
|
+
* frontend (to derive coverage state) and by the backend Meetch contract
|
|
8
|
+
* (`MEETCH_FLEX_CONTRACT.cancellationUntilHoursBeforeDeparture`).
|
|
9
|
+
*/
|
|
10
|
+
export const MEETCH_FLEX_CANCELLATION_HOURS_BEFORE_CHECK_IN = 48
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Persisted lifecycle status of an insurance subscription/claim. Shared so the
|
|
14
|
+
* frontend can derive cancellation UI from the same values the backend emits;
|
|
15
|
+
* re-exported from `@revolugo/node/constants` for server-side consumers.
|
|
16
|
+
*/
|
|
17
|
+
export enum InsuranceStatus {
|
|
18
|
+
Active = 'active',
|
|
19
|
+
Archived = 'archived',
|
|
20
|
+
Claimed = 'claimed',
|
|
21
|
+
Incomplete = 'incomplete',
|
|
22
|
+
Open = 'open',
|
|
23
|
+
WaitingPayment = 'waiting_payment',
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Insurance statuses for which a Meetch/Flex claim is considered "in progress":
|
|
28
|
+
* the claim has been opened or settled on the insurer side, so the booking
|
|
29
|
+
* manager swaps to the claim-status UI instead of offering the cancel CTA.
|
|
30
|
+
*/
|
|
31
|
+
export const MEETCH_CLAIM_IN_PROGRESS_STATUSES: InsuranceStatus[] = [
|
|
32
|
+
InsuranceStatus.Open,
|
|
33
|
+
InsuranceStatus.Claimed,
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* UI-level lifecycle state of a Meetch/Flex insured booking, used to pick which
|
|
38
|
+
* cancellation UI to render. Derived from {@link InsuranceStatus} plus the
|
|
39
|
+
* coverage window; `none` means the booking is not Flex-insured and the
|
|
40
|
+
* standard cancellation policy applies.
|
|
41
|
+
*/
|
|
42
|
+
export const MEETCH_INSURANCE_STATE = {
|
|
43
|
+
ClaimInProgress: 'claim-in-progress',
|
|
44
|
+
Expired: 'expired',
|
|
45
|
+
None: 'none',
|
|
46
|
+
Valid: 'valid',
|
|
47
|
+
} as const
|
|
48
|
+
|
|
49
|
+
export type MeetchInsuranceStateType =
|
|
50
|
+
(typeof MEETCH_INSURANCE_STATE)[keyof typeof MEETCH_INSURANCE_STATE]
|
package/src/constants/legal.ts
CHANGED
package/src/currencies/utils.ts
CHANGED
package/src/icons/index.ts
CHANGED
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
/* eslint-disable camelcase */
|
|
2
2
|
import { z } from 'zod'
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
BookingStatusEnum,
|
|
6
|
-
PayLaterStatusEnum,
|
|
7
|
-
PaymentMethodNameEnum,
|
|
8
|
-
} from '../types/index.ts'
|
|
4
|
+
import { PayLaterStatusEnum, PaymentMethodNameEnum } from '../types/index.ts'
|
|
9
5
|
import { isEqual } from '../utils/is-equal.ts'
|
|
10
6
|
|
|
11
7
|
const allowedPaymentMethodCombinations = [
|
|
@@ -110,7 +106,8 @@ export const PAYMENT_METHOD_RESPONSE_SCHEMA = z
|
|
|
110
106
|
export const PAYMENT_METHODS_RESPONSE_SCHEMA = z
|
|
111
107
|
.array(PAYMENT_METHOD_RESPONSE_SCHEMA)
|
|
112
108
|
.openapi('paymentMethodsApi', {
|
|
113
|
-
description:
|
|
109
|
+
description:
|
|
110
|
+
'List of preferred payment methods to be used along with their respective payload (when applicable) in order to fulfill the order',
|
|
114
111
|
})
|
|
115
112
|
.optional()
|
|
116
113
|
|
|
@@ -20,7 +20,7 @@ export type PaymentMethod =
|
|
|
20
20
|
| PaymentMethodPayLater
|
|
21
21
|
|
|
22
22
|
export interface BasePaymentMethodCreditCard extends BasePaymentMethod {
|
|
23
|
-
name:
|
|
23
|
+
name: PaymentMethodNameEnum.CreditCard
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export interface PaymentMethodCreditCard extends BasePaymentMethodCreditCard {
|
|
@@ -32,7 +32,7 @@ export interface PaymentMethodCreditCard extends BasePaymentMethodCreditCard {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
export interface BasePaymentMethodCoupon extends BasePaymentMethod {
|
|
35
|
-
name:
|
|
35
|
+
name: PaymentMethodNameEnum.Coupon
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
export interface PaymentMethodCoupon extends BasePaymentMethodCoupon {
|
|
@@ -43,7 +43,7 @@ export interface PaymentMethodCoupon extends BasePaymentMethodCoupon {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
export interface BasePaymentMethodDepositAccount extends BasePaymentMethod {
|
|
46
|
-
name:
|
|
46
|
+
name: PaymentMethodNameEnum.DepositAccount
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
export interface PaymentMethodDepositAccount extends BasePaymentMethodDepositAccount {
|
|
@@ -54,7 +54,7 @@ export interface PaymentMethodDepositAccount extends BasePaymentMethodDepositAcc
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
export interface BasePaymentMethodPayLater extends BasePaymentMethod {
|
|
57
|
-
name:
|
|
57
|
+
name: PaymentMethodNameEnum.PayLater
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
export interface PayloadPayLater {
|
package/src/types/insurance.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CurrencyCode } from '../constants/index.ts'
|
|
1
|
+
import type { CurrencyCode, InsuranceStatus } from '../constants/index.ts'
|
|
2
2
|
|
|
3
3
|
export interface InsuranceSummary {
|
|
4
4
|
currency: CurrencyCode
|
|
@@ -7,3 +7,20 @@ export interface InsuranceSummary {
|
|
|
7
7
|
price: number
|
|
8
8
|
taxIncludedPrice: number
|
|
9
9
|
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* View-model describing an in-progress Meetch/Flex insurance claim, surfaced in
|
|
13
|
+
* the booking manager cancel modal. Populated from booking-api claim data once
|
|
14
|
+
* that is exposed; while absent, the "claim in progress" state never renders.
|
|
15
|
+
*/
|
|
16
|
+
export interface MeetchClaimViewModel {
|
|
17
|
+
currency: CurrencyCode
|
|
18
|
+
email: string
|
|
19
|
+
reference: string
|
|
20
|
+
/** Expected refund amount in minor units, in {@link MeetchClaimViewModel.currency}. */
|
|
21
|
+
refundAmount: number
|
|
22
|
+
/** Persisted claim status; the UI maps it to a localized label + tag severity. */
|
|
23
|
+
status: InsuranceStatus
|
|
24
|
+
/** ISO datetime the claim was submitted — rendered as "Claim submitted {date}". */
|
|
25
|
+
submittedAt: string
|
|
26
|
+
}
|