@owlmeans/web-payment 0.1.18-rc.20 → 0.1.18-rc.21
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/README.md +1 -1
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/web-payment/SKILL.md +171 -31
- package/build/@/components/ui/checkbox.d.ts +5 -0
- package/build/@/components/ui/checkbox.d.ts.map +1 -0
- package/build/@/components/ui/checkbox.js +6 -0
- package/build/@/components/ui/checkbox.js.map +1 -0
- package/build/checkout-limit.d.ts +11 -0
- package/build/checkout-limit.d.ts.map +1 -0
- package/build/checkout-limit.js +34 -0
- package/build/checkout-limit.js.map +1 -0
- package/build/consumer/cancellation.d.ts +20 -0
- package/build/consumer/cancellation.d.ts.map +1 -0
- package/build/consumer/cancellation.js +115 -0
- package/build/consumer/cancellation.js.map +1 -0
- package/build/consumer/copy.d.ts +23 -0
- package/build/consumer/copy.d.ts.map +1 -0
- package/build/consumer/copy.js +75 -0
- package/build/consumer/copy.js.map +1 -0
- package/build/consumer/ensure.d.ts +56 -0
- package/build/consumer/ensure.d.ts.map +1 -0
- package/build/consumer/ensure.js +71 -0
- package/build/consumer/ensure.js.map +1 -0
- package/build/consumer/fields.d.ts +43 -0
- package/build/consumer/fields.d.ts.map +1 -0
- package/build/consumer/fields.js +19 -0
- package/build/consumer/fields.js.map +1 -0
- package/build/consumer/hooks.d.ts +113 -0
- package/build/consumer/hooks.d.ts.map +1 -0
- package/build/consumer/hooks.js +225 -0
- package/build/consumer/hooks.js.map +1 -0
- package/build/consumer/index.d.ts +14 -0
- package/build/consumer/index.d.ts.map +1 -0
- package/build/consumer/index.js +12 -0
- package/build/consumer/index.js.map +1 -0
- package/build/consumer/legal.d.ts +53 -0
- package/build/consumer/legal.d.ts.map +1 -0
- package/build/consumer/legal.js +77 -0
- package/build/consumer/legal.js.map +1 -0
- package/build/consumer/performance-consent-dialog.d.ts +18 -0
- package/build/consumer/performance-consent-dialog.d.ts.map +1 -0
- package/build/consumer/performance-consent-dialog.js +71 -0
- package/build/consumer/performance-consent-dialog.js.map +1 -0
- package/build/consumer/provider.d.ts +33 -0
- package/build/consumer/provider.d.ts.map +1 -0
- package/build/consumer/provider.js +26 -0
- package/build/consumer/provider.js.map +1 -0
- package/build/consumer/refusal.d.ts +35 -0
- package/build/consumer/refusal.d.ts.map +1 -0
- package/build/consumer/refusal.js +98 -0
- package/build/consumer/refusal.js.map +1 -0
- package/build/consumer/subscription-start-dialog.d.ts +16 -0
- package/build/consumer/subscription-start-dialog.d.ts.map +1 -0
- package/build/consumer/subscription-start-dialog.js +43 -0
- package/build/consumer/subscription-start-dialog.js.map +1 -0
- package/build/consumer/types.d.ts +114 -0
- package/build/consumer/types.d.ts.map +1 -0
- package/build/consumer/types.js +2 -0
- package/build/consumer/types.js.map +1 -0
- package/build/consumer/withdrawal.d.ts +29 -0
- package/build/consumer/withdrawal.d.ts.map +1 -0
- package/build/consumer/withdrawal.js +140 -0
- package/build/consumer/withdrawal.js.map +1 -0
- package/build/dialog.d.ts +1 -1
- package/build/dialog.d.ts.map +1 -1
- package/build/dialog.js +42 -16
- package/build/dialog.js.map +1 -1
- package/build/estimate-summary.d.ts +9 -1
- package/build/estimate-summary.d.ts.map +1 -1
- package/build/estimate-summary.js +10 -7
- package/build/estimate-summary.js.map +1 -1
- package/build/estimate.d.ts +6 -0
- package/build/estimate.d.ts.map +1 -1
- package/build/estimate.js +26 -1
- package/build/estimate.js.map +1 -1
- package/build/format.d.ts +15 -0
- package/build/format.d.ts.map +1 -0
- package/build/format.js +68 -0
- package/build/format.js.map +1 -0
- package/build/i18n/be.json +1 -1
- package/build/i18n/de.json +1 -1
- package/build/i18n/en.json +1 -1
- package/build/i18n/es.json +1 -1
- package/build/i18n/fr.json +110 -15
- package/build/i18n/pl.json +1 -1
- package/build/i18n/ru.json +1 -1
- package/build/i18n/uk.json +1 -1
- package/build/i18n.d.ts +2 -1
- package/build/i18n.d.ts.map +1 -1
- package/build/i18n.js +10 -8
- package/build/i18n.js.map +1 -1
- package/build/index.d.ts +2 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +2 -0
- package/build/index.js.map +1 -1
- package/build/types.d.ts +22 -1
- package/build/types.d.ts.map +1 -1
- package/package.json +18 -7
- package/src/@/components/ui/checkbox.tsx +23 -0
- package/src/checkout-limit.tsx +48 -0
- package/src/consumer/cancellation.tsx +214 -0
- package/src/consumer/copy.ts +93 -0
- package/src/consumer/ensure.ts +124 -0
- package/src/consumer/fields.tsx +89 -0
- package/src/consumer/hooks.ts +358 -0
- package/src/consumer/index.ts +13 -0
- package/src/consumer/legal.tsx +129 -0
- package/src/consumer/performance-consent-dialog.tsx +132 -0
- package/src/consumer/provider.tsx +49 -0
- package/src/consumer/refusal.ts +110 -0
- package/src/consumer/subscription-start-dialog.tsx +87 -0
- package/src/consumer/types.ts +121 -0
- package/src/consumer/withdrawal.tsx +279 -0
- package/src/dialog.tsx +53 -23
- package/src/estimate-summary.tsx +21 -11
- package/src/estimate.ts +27 -1
- package/src/format.ts +71 -0
- package/src/i18n/be.json +1 -1
- package/src/i18n/de.json +1 -1
- package/src/i18n/en.json +1 -1
- package/src/i18n/es.json +1 -1
- package/src/i18n/fr.json +110 -15
- package/src/i18n/pl.json +1 -1
- package/src/i18n/ru.json +1 -1
- package/src/i18n/uk.json +1 -1
- package/src/i18n.ts +11 -8
- package/src/index.ts +2 -0
- package/src/types.ts +23 -1
- package/tests/consent-refusal.spec.ts +83 -0
- package/tests/consumer-hooks.spec.ts +205 -0
- package/tests/consumer.spec.ts +343 -0
- package/tests/context.ts +1 -1
- package/tests/harness/consumer-fixtures.ts +105 -0
- package/tests/harness/consumer.tsx +197 -0
- package/tests/harness/mount.tsx +9 -2
- package/tests/i18n.spec.ts +41 -5
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { useState, type ReactElement } from 'react'
|
|
2
|
+
import type { RegisteredEntrypoint } from '@owlmeans/entrypoint'
|
|
3
|
+
import type {
|
|
4
|
+
CancellationBody, DeclarationReceipt, PerformanceConsentBody, PerformanceConsentResponse, PerformanceConsentView,
|
|
5
|
+
PriceEstimate, WithdrawalBody, WithdrawalCandidateList,
|
|
6
|
+
} from '@owlmeans/payment'
|
|
7
|
+
import { AmountCheckoutDialog, PriceEstimateSummary, usePriceEstimate } from '../../src/index.js'
|
|
8
|
+
import {
|
|
9
|
+
CancellationForm, PerformanceConsentDialog, PerformanceConsentProvider, SubscriptionStartDialog, WithdrawalDialog,
|
|
10
|
+
WithdrawalForm, WithdrawalFunctionButton, isConsentDeclined, useCancellation, useConsentGate, useWithdrawal,
|
|
11
|
+
} from '../../src/consumer/index.js'
|
|
12
|
+
import {
|
|
13
|
+
BLOCKED_LIMIT, LINKS, PER_PURCHASE_LIMIT, POLICY, cancellationReceipt, consentView, lockedEstimate, publicReceipt,
|
|
14
|
+
startView, withdrawalList, withdrawalReceipt,
|
|
15
|
+
} from './consumer-fixtures.js'
|
|
16
|
+
|
|
17
|
+
const params = new URLSearchParams(window.location.search)
|
|
18
|
+
const lng = params.get('lng') ?? 'en'
|
|
19
|
+
|
|
20
|
+
/** A fake bound protocol: records the request and answers after a short, real delay. */
|
|
21
|
+
const fake = <Response,>(answer: (request: { body?: unknown }) => Response, record?: (request: unknown) => void) => ({
|
|
22
|
+
call: async (request: { body?: unknown } = {}) => {
|
|
23
|
+
record?.(request)
|
|
24
|
+
await new Promise(resolve => setTimeout(resolve, 50))
|
|
25
|
+
return answer(request)
|
|
26
|
+
},
|
|
27
|
+
}) as unknown as RegisteredEntrypoint<any, Response>
|
|
28
|
+
|
|
29
|
+
const Output = ({ id, value }: { id: string, value: unknown }) =>
|
|
30
|
+
<output id={id}>{value == null ? '' : typeof value === 'string' ? value : JSON.stringify(value)}</output>
|
|
31
|
+
|
|
32
|
+
const ConsentCase = () => {
|
|
33
|
+
const [open, setOpen] = useState(true)
|
|
34
|
+
const [result, setResult] = useState<unknown>(null)
|
|
35
|
+
return <main className="p-6">
|
|
36
|
+
<Output id="consent-result" value={result} />
|
|
37
|
+
<Output id="dialog-state" value={open ? 'open' : 'closed'} />
|
|
38
|
+
<PerformanceConsentDialog
|
|
39
|
+
open={open} onOpenChange={setOpen} view={consentView(lng)} links={LINKS}
|
|
40
|
+
onConfirm={body => { setResult(body); setOpen(false) }}
|
|
41
|
+
onDecline={() => { setResult('declined'); setOpen(false) }}
|
|
42
|
+
/>
|
|
43
|
+
</main>
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const StartCase = () => {
|
|
47
|
+
const [open, setOpen] = useState(true)
|
|
48
|
+
const [result, setResult] = useState<unknown>(null)
|
|
49
|
+
return <main className="p-6">
|
|
50
|
+
<Output id="start-result" value={result} />
|
|
51
|
+
<SubscriptionStartDialog
|
|
52
|
+
open={open} onOpenChange={setOpen} view={startView(lng)} planTitle="Pro" links={LINKS}
|
|
53
|
+
price={<span>€20.00 / month</span>}
|
|
54
|
+
onConfirm={body => { setResult(body); setOpen(false) }}
|
|
55
|
+
onDecline={() => { setResult('declined'); setOpen(false) }}
|
|
56
|
+
/>
|
|
57
|
+
</main>
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const WithdrawalCase = () => {
|
|
61
|
+
const [open, setOpen] = useState(false)
|
|
62
|
+
const [sent, setSent] = useState<unknown>(null)
|
|
63
|
+
const [entries] = useState(() => ({
|
|
64
|
+
list: fake<WithdrawalCandidateList>(() => withdrawalList(lng)),
|
|
65
|
+
withdraw: fake<DeclarationReceipt>(() => withdrawalReceipt, request => setSent((request as { body: WithdrawalBody }).body)),
|
|
66
|
+
}))
|
|
67
|
+
const withdrawal = useWithdrawal(entries.list, entries.withdraw, { enabled: true })
|
|
68
|
+
return <main className="grid gap-4 p-6">
|
|
69
|
+
<Output id="withdrawal-body" value={sent} />
|
|
70
|
+
<div><WithdrawalFunctionButton language={lng} onClick={() => setOpen(true)} /></div>
|
|
71
|
+
<WithdrawalDialog
|
|
72
|
+
open={open} onOpenChange={next => { setOpen(next); if (!next) withdrawal.reset() }}
|
|
73
|
+
mode="in-app" language={lng} {...withdrawal.form}
|
|
74
|
+
/>
|
|
75
|
+
</main>
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const WithdrawalPublicCase = () => {
|
|
79
|
+
const [sent, setSent] = useState<unknown>(null)
|
|
80
|
+
const [entry] = useState(() => fake<DeclarationReceipt>(() => publicReceipt, request => setSent((request as { body: WithdrawalBody }).body)))
|
|
81
|
+
const withdrawal = useWithdrawal(null, entry)
|
|
82
|
+
return <main className="p-6">
|
|
83
|
+
<Output id="withdrawal-body" value={sent} />
|
|
84
|
+
<WithdrawalForm mode="public" language={lng} {...withdrawal.form} />
|
|
85
|
+
</main>
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const CancellationCase = () => {
|
|
89
|
+
const [sent, setSent] = useState<unknown>(null)
|
|
90
|
+
const [printed, setPrinted] = useState(false)
|
|
91
|
+
const [entry] = useState(() => fake<DeclarationReceipt>(() => cancellationReceipt, request => setSent((request as { body: CancellationBody }).body)))
|
|
92
|
+
const cancellation = useCancellation(entry)
|
|
93
|
+
return <main className="p-6">
|
|
94
|
+
<Output id="cancellation-body" value={sent} />
|
|
95
|
+
<Output id="printed" value={printed ? 'printed' : ''} />
|
|
96
|
+
<CancellationForm mode="public" language={lng} onPrint={() => setPrinted(true)} {...cancellation.form} />
|
|
97
|
+
</main>
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const LimitCase = () => {
|
|
101
|
+
const [open, setOpen] = useState(true)
|
|
102
|
+
const [confirmed, setConfirmed] = useState<number | null>(null)
|
|
103
|
+
const blocked = params.get('blocked') === 'true'
|
|
104
|
+
return <main className="p-6">
|
|
105
|
+
<Output id="confirmed" value={confirmed == null ? '' : String(confirmed)} />
|
|
106
|
+
<AmountCheckoutDialog
|
|
107
|
+
open={open} onOpenChange={setOpen} policy={POLICY} onConfirm={setConfirmed}
|
|
108
|
+
limit={blocked ? BLOCKED_LIMIT : PER_PURCHASE_LIMIT}
|
|
109
|
+
legalNote={<span data-harness-legal-note="">Only unused credits are reimbursed on withdrawal.</span>}
|
|
110
|
+
/>
|
|
111
|
+
</main>
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const CountryLockedCase = () => {
|
|
115
|
+
const [requested, setRequested] = useState<string[]>([])
|
|
116
|
+
const [entry] = useState(() => fake<PriceEstimate>(
|
|
117
|
+
() => lockedEstimate(2_000),
|
|
118
|
+
request => setRequested(held => [...held, (request as { body?: { country?: string } }).body?.country ?? '']),
|
|
119
|
+
))
|
|
120
|
+
const estimate = usePriceEstimate(entry, { enabled: true })
|
|
121
|
+
return <main className="p-6">
|
|
122
|
+
<Output id="requested" value={requested} />
|
|
123
|
+
<Output id="locked" value={String(estimate.locked === true)} />
|
|
124
|
+
<PriceEstimateSummary control={estimate} subtotalMinor={2_000} currency="usd" />
|
|
125
|
+
</main>
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** `withConsent` against a protocol refused ONCE with a bare 428 — a production incident body. */
|
|
129
|
+
const GateButtons = () => {
|
|
130
|
+
const gate = useConsentGate()
|
|
131
|
+
const [result, setResult] = useState('')
|
|
132
|
+
const [calls, setCalls] = useState(0)
|
|
133
|
+
const [refusals] = useState(() => ({ left: 1 }))
|
|
134
|
+
const run = async () => {
|
|
135
|
+
let made = 0
|
|
136
|
+
try {
|
|
137
|
+
await gate.withConsent(async () => {
|
|
138
|
+
made++
|
|
139
|
+
setCalls(made)
|
|
140
|
+
if (refusals.left > 0) {
|
|
141
|
+
refusals.left--
|
|
142
|
+
throw new Error('api:client:status:428:0b6f8a3e-8f0e-4b9f-9c55-2f1f0f6f2a11')
|
|
143
|
+
}
|
|
144
|
+
return 'done'
|
|
145
|
+
})
|
|
146
|
+
setResult(`ok:${made}`)
|
|
147
|
+
} catch (e) {
|
|
148
|
+
setResult(isConsentDeclined(e) ? `declined:${made}` : `error:${made}`)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const ensure = async () => { setResult(`ensure:${String(await gate.ensure())}`) }
|
|
152
|
+
return <>
|
|
153
|
+
<Output id="gate-result" value={result} />
|
|
154
|
+
<Output id="gate-calls" value={String(calls)} />
|
|
155
|
+
<button type="button" id="gate-run" onClick={() => void run()}>Run</button>
|
|
156
|
+
<button type="button" id="gate-ensure" onClick={() => void ensure()}>Ensure</button>
|
|
157
|
+
</>
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const ConsentGateCase = () => {
|
|
161
|
+
const [state] = useState(() => ({ consented: false }))
|
|
162
|
+
const [recorded, setRecorded] = useState<unknown>(null)
|
|
163
|
+
const [entries] = useState(() => ({
|
|
164
|
+
view: fake<PerformanceConsentView>(() => JSON.parse(JSON.stringify(
|
|
165
|
+
state.consented ? { ...consentView(lng), required: false, purchases: [] } : consentView(lng),
|
|
166
|
+
))),
|
|
167
|
+
record: fake<PerformanceConsentResponse>(request => {
|
|
168
|
+
state.consented = true
|
|
169
|
+
const body = (request as { body: PerformanceConsentBody }).body
|
|
170
|
+
return { consentId: 'cons_1', consentedAt: new Date().toISOString(), purchaseIds: body.purchaseIds, mailed: true } as never
|
|
171
|
+
}, request => setRecorded((request as { body: unknown }).body)),
|
|
172
|
+
}))
|
|
173
|
+
return <main className="grid gap-2 p-6">
|
|
174
|
+
<Output id="recorded" value={recorded} />
|
|
175
|
+
<PerformanceConsentProvider view={entries.view} record={entries.record} options={{ links: LINKS }}>
|
|
176
|
+
<GateButtons />
|
|
177
|
+
</PerformanceConsentProvider>
|
|
178
|
+
</main>
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const CASES: Record<string, () => ReactElement> = {
|
|
182
|
+
consent: ConsentCase,
|
|
183
|
+
start: StartCase,
|
|
184
|
+
withdrawal: WithdrawalCase,
|
|
185
|
+
'withdrawal-public': WithdrawalPublicCase,
|
|
186
|
+
cancellation: CancellationCase,
|
|
187
|
+
limit: LimitCase,
|
|
188
|
+
'country-locked': CountryLockedCase,
|
|
189
|
+
'consent-gate': ConsentGateCase,
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export const isConsumerCase = (name: string | null): name is string => name != null && name in CASES
|
|
193
|
+
|
|
194
|
+
export const ConsumerCase = ({ name }: { name: string }) => {
|
|
195
|
+
const Case = CASES[name]
|
|
196
|
+
return <Case />
|
|
197
|
+
}
|
package/tests/harness/mount.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import { useState } from 'react'
|
|
|
2
2
|
import { createRoot } from 'react-dom/client'
|
|
3
3
|
import { I18nContext } from '@owlmeans/client-i18n'
|
|
4
4
|
import { AmountCheckoutDialog } from '../../src/index.js'
|
|
5
|
+
import { ConsumerCase, isConsumerCase } from './consumer.js'
|
|
5
6
|
import { EntitlementCase } from './entitlement.js'
|
|
6
7
|
import { ESTIMATE_FIXTURES } from './estimate-fixtures.js'
|
|
7
8
|
|
|
@@ -31,8 +32,14 @@ const Dialog = () => {
|
|
|
31
32
|
</>
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
/** `?ui=` is the interface language; `?lng=` (read by the consumer cases) the contract language. */
|
|
36
|
+
const ui = params.get('ui') ?? 'en'
|
|
37
|
+
const LANGUAGES = ['en', 'pl', 'ru', 'be', 'uk', 'es', 'de', 'fr']
|
|
38
|
+
|
|
39
|
+
const App = () => <I18nContext config={{ service: 'web-payment-test', i18n: { defaultLng: ui, supportedLngs: LANGUAGES } } as never}>
|
|
40
|
+
{scenario == null
|
|
41
|
+
? <Dialog />
|
|
42
|
+
: isConsumerCase(scenario) ? <ConsumerCase name={scenario} /> : <EntitlementCase name={scenario} />}
|
|
36
43
|
</I18nContext>
|
|
37
44
|
|
|
38
45
|
createRoot(document.getElementById('root')!).render(<App />)
|
package/tests/i18n.spec.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { CancellationStatus, PurchaseKind, WithdrawalStatus } from '@owlmeans/payment'
|
|
2
3
|
import en from '../src/i18n/en.json'
|
|
3
4
|
import pl from '../src/i18n/pl.json'
|
|
4
5
|
import ru from '../src/i18n/ru.json'
|
|
@@ -6,6 +7,7 @@ import be from '../src/i18n/be.json'
|
|
|
6
7
|
import uk from '../src/i18n/uk.json'
|
|
7
8
|
import es from '../src/i18n/es.json'
|
|
8
9
|
import de from '../src/i18n/de.json'
|
|
10
|
+
import fr from '../src/i18n/fr.json'
|
|
9
11
|
|
|
10
12
|
type Messages = { [key: string]: string | Messages }
|
|
11
13
|
|
|
@@ -26,14 +28,25 @@ const leaves = (messages: Messages, prefix = ''): Map<string, unknown> => {
|
|
|
26
28
|
const placeholders = (message: string): string[] =>
|
|
27
29
|
[...message.matchAll(/\{\{\s*([\w-]+)\s*\}\}/g)].map(match => match[1]).sort()
|
|
28
30
|
|
|
31
|
+
/**
|
|
32
|
+
* What no payment text may say in any language (UCPD art. 6(1)(g)): the right of withdrawal
|
|
33
|
+
* EXPIRES for what was used, and unused credits ARE reimbursed — nothing is "non-refundable".
|
|
34
|
+
*/
|
|
35
|
+
const FORBIDDEN = /non-?refundable|nicht erstattungsf|non rembours|bezzwrotn|no reembolsable|невозвратн|неповоротн|незваротн/i
|
|
36
|
+
|
|
29
37
|
describe('web-payment localization', () => {
|
|
30
|
-
const translations: Record<string, Messages> = { en, pl, ru, be, uk, es, de }
|
|
38
|
+
const translations: Record<string, Messages> = { en, pl, ru, be, uk, es, de, fr }
|
|
31
39
|
const reference = leaves(en)
|
|
32
40
|
|
|
33
|
-
test('ships all
|
|
34
|
-
expect(Object.keys(translations).sort()).toEqual(['be', 'de', 'en', 'es', 'pl', 'ru', 'uk'])
|
|
35
|
-
|
|
36
|
-
|
|
41
|
+
test('ships all eight library languages with exact key parity at every depth', () => {
|
|
42
|
+
expect(Object.keys(translations).sort()).toEqual(['be', 'de', 'en', 'es', 'fr', 'pl', 'ru', 'uk'])
|
|
43
|
+
for (const key of [
|
|
44
|
+
'amount-checkout.title', 'amount-checkout.above-limit', 'entitlement.status.renews-on', 'estimate.country-locked',
|
|
45
|
+
'checkout-limit.note', 'checkout-limit.reason.per-purchase', 'checkout-limit.reason.window', 'checkout-limit.reason.hold',
|
|
46
|
+
'consumer.show-in', 'consumer.review', 'consumer.receipt', 'withdrawal.status.refunded', 'cancellation.status.scheduled',
|
|
47
|
+
]) {
|
|
48
|
+
expect({ key, present: reference.has(key) }).toEqual({ key, present: true })
|
|
49
|
+
}
|
|
37
50
|
for (const [lng, messages] of Object.entries(translations)) {
|
|
38
51
|
const found = leaves(messages)
|
|
39
52
|
expect({ lng, keys: [...found.keys()].sort() }).toEqual({ lng, keys: [...reference.keys()].sort() })
|
|
@@ -52,4 +65,27 @@ describe('web-payment localization', () => {
|
|
|
52
65
|
}
|
|
53
66
|
}
|
|
54
67
|
})
|
|
68
|
+
|
|
69
|
+
test('phrases every status and kind a receipt or a candidate can carry, in every language', () => {
|
|
70
|
+
const required = [
|
|
71
|
+
...Object.values(WithdrawalStatus).map(status => `withdrawal.status.${status}`),
|
|
72
|
+
...Object.values(CancellationStatus).map(status => `cancellation.status.${status}`),
|
|
73
|
+
...Object.values(PurchaseKind).map(kind => `withdrawal.kind.${kind}`),
|
|
74
|
+
]
|
|
75
|
+
expect(required).toContain('cancellation.status.review')
|
|
76
|
+
for (const [lng, messages] of Object.entries(translations)) {
|
|
77
|
+
const found = leaves(messages)
|
|
78
|
+
for (const key of required) {
|
|
79
|
+
expect({ lng, key, present: typeof found.get(key) === 'string' }).toEqual({ lng, key, present: true })
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test('never calls anything non-refundable, in any language', () => {
|
|
85
|
+
for (const [lng, messages] of Object.entries(translations)) {
|
|
86
|
+
for (const [key, message] of leaves(messages)) {
|
|
87
|
+
expect({ lng, key, forbidden: FORBIDDEN.test(String(message)) }).toEqual({ lng, key, forbidden: false })
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
})
|
|
55
91
|
})
|