@monimesl/dot-cli 0.1.0-alpha.8 → 1.0.0-beta.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.
Files changed (162) hide show
  1. package/README.md +75 -54
  2. package/dist/cli.js +628 -348
  3. package/dist/cli.js.map +1 -1
  4. package/dist/{server-DFOUQnLA.js → server-DMnWDdJ6.js} +582 -537
  5. package/dist/server-DMnWDdJ6.js.map +1 -0
  6. package/dist/templates/showcase/.env +1 -1
  7. package/dist/templates/showcase/dot.ts +53 -14
  8. package/dist/templates/showcase/pages/basics/bars/controller.ts +47 -0
  9. package/dist/templates/showcase/pages/basics/bars/state.ts +7 -0
  10. package/dist/templates/showcase/pages/basics/bars/view.dml +36 -0
  11. package/dist/templates/showcase/pages/basics/bindings/controller.ts +15 -0
  12. package/dist/templates/showcase/pages/basics/bindings/state.ts +14 -0
  13. package/dist/templates/showcase/pages/basics/bindings/view.dml +41 -0
  14. package/dist/templates/showcase/pages/basics/colors/controller.ts +112 -0
  15. package/dist/templates/showcase/pages/basics/colors/palette.ts +52 -0
  16. package/dist/templates/showcase/pages/basics/colors/state.ts +17 -0
  17. package/dist/templates/showcase/pages/basics/colors/view.dml +142 -0
  18. package/dist/templates/showcase/pages/basics/concert/controller.ts +16 -0
  19. package/dist/templates/showcase/pages/basics/concert/state.ts +7 -0
  20. package/dist/templates/showcase/pages/basics/concert/view.dml +20 -0
  21. package/dist/templates/showcase/pages/basics/control-flow/controller.ts +18 -0
  22. package/dist/templates/showcase/pages/basics/control-flow/state.ts +16 -0
  23. package/dist/templates/showcase/pages/basics/control-flow/view.dml +55 -0
  24. package/dist/templates/showcase/pages/basics/events/controller.ts +43 -0
  25. package/dist/templates/showcase/pages/basics/events/state.ts +11 -0
  26. package/dist/templates/showcase/pages/basics/events/view.dml +51 -0
  27. package/dist/templates/showcase/pages/basics/hooks/controller.ts +43 -0
  28. package/dist/templates/showcase/pages/basics/hooks/state.ts +10 -0
  29. package/dist/templates/showcase/pages/basics/hooks/view.dml +37 -0
  30. package/dist/templates/showcase/pages/basics/navigation/controller.ts +41 -0
  31. package/dist/templates/showcase/pages/basics/navigation/state.ts +11 -0
  32. package/dist/templates/showcase/pages/basics/navigation/view.dml +48 -0
  33. package/dist/templates/showcase/pages/basics/pick-genre/controller.ts +18 -0
  34. package/dist/templates/showcase/pages/basics/pick-genre/state.ts +3 -0
  35. package/dist/templates/showcase/pages/basics/pick-genre/view.dml +10 -0
  36. package/dist/templates/showcase/pages/basics/share/concerts.ts +31 -0
  37. package/dist/templates/showcase/pages/basics/share/controller.ts +12 -0
  38. package/dist/templates/showcase/pages/basics/share/state.ts +5 -0
  39. package/dist/templates/showcase/pages/basics/share/view.dml +21 -0
  40. package/dist/templates/showcase/pages/{state → basics/state}/controller.ts +1 -0
  41. package/dist/templates/showcase/pages/{state → basics/state}/state.ts +1 -1
  42. package/dist/templates/showcase/pages/basics/state/view.dml +46 -0
  43. package/dist/templates/showcase/pages/components/carousel/controller.ts +41 -0
  44. package/dist/templates/showcase/pages/components/carousel/state.ts +77 -0
  45. package/dist/templates/showcase/pages/components/carousel/view.dml +42 -0
  46. package/dist/templates/showcase/pages/components/feedback/controller.ts +32 -0
  47. package/dist/templates/showcase/pages/components/feedback/state.ts +41 -0
  48. package/dist/templates/showcase/pages/components/feedback/view.dml +116 -0
  49. package/dist/templates/showcase/pages/components/forms/controller.ts +72 -0
  50. package/dist/templates/showcase/pages/components/forms/state.ts +37 -0
  51. package/dist/templates/showcase/pages/components/forms/view.dml +97 -0
  52. package/dist/templates/showcase/pages/components/icons/controller.ts +15 -0
  53. package/dist/templates/showcase/pages/components/icons/state.ts +75 -0
  54. package/dist/templates/showcase/pages/components/icons/view.dml +36 -0
  55. package/dist/templates/showcase/pages/components/images/controller.ts +44 -0
  56. package/dist/templates/showcase/pages/{images → components/images}/state.ts +3 -0
  57. package/dist/templates/showcase/pages/components/images/view.dml +83 -0
  58. package/dist/templates/showcase/pages/components/layout/controller.ts +61 -0
  59. package/dist/templates/showcase/pages/components/layout/state.ts +34 -0
  60. package/dist/templates/showcase/pages/components/layout/view.dml +143 -0
  61. package/dist/templates/showcase/pages/components/lists/controller.ts +18 -0
  62. package/dist/templates/showcase/pages/components/lists/state.ts +130 -0
  63. package/dist/templates/showcase/pages/components/lists/view.dml +88 -0
  64. package/dist/templates/showcase/pages/components/money/controller.ts +45 -0
  65. package/dist/templates/showcase/pages/{payments → components/money}/state.ts +3 -0
  66. package/dist/templates/showcase/pages/components/money/view.dml +54 -0
  67. package/dist/templates/showcase/pages/components/reading/controller.ts +6 -0
  68. package/dist/templates/showcase/pages/components/reading/state.ts +23 -0
  69. package/dist/templates/showcase/pages/components/reading/view.dml +42 -0
  70. package/dist/templates/showcase/pages/components/sheets/controller.ts +36 -0
  71. package/dist/templates/showcase/pages/components/sheets/state.ts +26 -0
  72. package/dist/templates/showcase/pages/components/sheets/view.dml +38 -0
  73. package/dist/templates/showcase/pages/components/styles/controller.ts +6 -0
  74. package/dist/templates/showcase/pages/components/styles/state.ts +3 -0
  75. package/dist/templates/showcase/pages/components/styles/view.dml +101 -0
  76. package/dist/templates/showcase/pages/components/tabs/controller.ts +41 -0
  77. package/dist/templates/showcase/pages/components/tabs/state.ts +127 -0
  78. package/dist/templates/showcase/pages/components/tabs/view.dml +57 -0
  79. package/dist/templates/showcase/pages/components/text/controller.ts +10 -0
  80. package/dist/templates/showcase/pages/components/text/state.ts +3 -0
  81. package/dist/templates/showcase/pages/components/text/view.dml +95 -0
  82. package/dist/templates/showcase/pages/dash/account/controller.ts +32 -0
  83. package/dist/templates/showcase/pages/dash/account/state.ts +9 -0
  84. package/dist/templates/showcase/pages/dash/account/view.dml +33 -0
  85. package/dist/templates/showcase/pages/dash/cache/controller.ts +50 -0
  86. package/dist/templates/showcase/pages/dash/cache/state.ts +17 -0
  87. package/dist/templates/showcase/pages/dash/cache/view.dml +48 -0
  88. package/dist/templates/showcase/pages/dash/calls/controller.ts +67 -0
  89. package/dist/templates/showcase/pages/dash/calls/state.ts +9 -0
  90. package/dist/templates/showcase/pages/dash/calls/view.dml +69 -0
  91. package/dist/templates/showcase/pages/dash/contacts/controller.ts +30 -0
  92. package/dist/templates/showcase/pages/dash/contacts/state.ts +6 -0
  93. package/dist/templates/showcase/pages/dash/contacts/view.dml +33 -0
  94. package/dist/templates/showcase/pages/dash/crypto/controller.ts +38 -0
  95. package/dist/templates/showcase/pages/dash/crypto/state.ts +10 -0
  96. package/dist/templates/showcase/pages/dash/crypto/view.dml +48 -0
  97. package/dist/templates/showcase/pages/dash/files/controller.ts +71 -0
  98. package/dist/templates/showcase/pages/dash/files/state.ts +23 -0
  99. package/dist/templates/showcase/pages/dash/files/view.dml +78 -0
  100. package/dist/templates/showcase/pages/dash/media/controller.ts +65 -0
  101. package/dist/templates/showcase/pages/dash/media/state.ts +12 -0
  102. package/dist/templates/showcase/pages/dash/media/view.dml +53 -0
  103. package/dist/templates/showcase/pages/dash/network/controller.ts +215 -0
  104. package/dist/templates/showcase/pages/dash/network/state.ts +35 -0
  105. package/dist/templates/showcase/pages/dash/network/view.dml +108 -0
  106. package/dist/templates/showcase/pages/dash/payments/cases.ts +445 -0
  107. package/dist/templates/showcase/pages/dash/payments/controller.ts +149 -0
  108. package/dist/templates/showcase/pages/dash/payments/state.ts +33 -0
  109. package/dist/templates/showcase/pages/dash/payments/view.dml +71 -0
  110. package/dist/templates/showcase/pages/dash/secrets/controller.ts +44 -0
  111. package/dist/templates/showcase/pages/dash/secrets/state.ts +5 -0
  112. package/dist/templates/showcase/pages/dash/secrets/view.dml +26 -0
  113. package/dist/templates/showcase/pages/dash/system/controller.ts +89 -0
  114. package/dist/templates/showcase/pages/dash/system/state.ts +13 -0
  115. package/dist/templates/showcase/pages/dash/system/view.dml +96 -0
  116. package/dist/templates/showcase/pages/dash/ui/controller.ts +70 -0
  117. package/dist/templates/showcase/pages/dash/ui/state.ts +8 -0
  118. package/dist/templates/showcase/pages/dash/ui/view.dml +31 -0
  119. package/dist/templates/showcase/pages/index/controller.ts +1 -1
  120. package/dist/templates/showcase/pages/index/state.ts +181 -54
  121. package/dist/templates/showcase/pages/index/view.dml +8 -5
  122. package/dist/templates/showcase/statics/logo.png +0 -0
  123. package/package.json +6 -6
  124. package/dist/server-DFOUQnLA.js.map +0 -1
  125. package/dist/templates/showcase/pages/carousel/controller.ts +0 -10
  126. package/dist/templates/showcase/pages/carousel/state.ts +0 -40
  127. package/dist/templates/showcase/pages/carousel/view.dml +0 -37
  128. package/dist/templates/showcase/pages/device/controller.ts +0 -70
  129. package/dist/templates/showcase/pages/device/state.ts +0 -7
  130. package/dist/templates/showcase/pages/device/view.dml +0 -44
  131. package/dist/templates/showcase/pages/feedback/controller.ts +0 -16
  132. package/dist/templates/showcase/pages/feedback/state.ts +0 -7
  133. package/dist/templates/showcase/pages/feedback/view.dml +0 -87
  134. package/dist/templates/showcase/pages/files/controller.ts +0 -47
  135. package/dist/templates/showcase/pages/files/state.ts +0 -11
  136. package/dist/templates/showcase/pages/files/view.dml +0 -35
  137. package/dist/templates/showcase/pages/filters/controller.ts +0 -28
  138. package/dist/templates/showcase/pages/filters/state.ts +0 -72
  139. package/dist/templates/showcase/pages/filters/view.dml +0 -43
  140. package/dist/templates/showcase/pages/forms/controller.ts +0 -44
  141. package/dist/templates/showcase/pages/forms/state.ts +0 -15
  142. package/dist/templates/showcase/pages/forms/view.dml +0 -39
  143. package/dist/templates/showcase/pages/images/controller.ts +0 -15
  144. package/dist/templates/showcase/pages/images/view.dml +0 -38
  145. package/dist/templates/showcase/pages/layout/controller.ts +0 -26
  146. package/dist/templates/showcase/pages/layout/state.ts +0 -14
  147. package/dist/templates/showcase/pages/layout/view.dml +0 -80
  148. package/dist/templates/showcase/pages/lists/controller.ts +0 -36
  149. package/dist/templates/showcase/pages/lists/state.ts +0 -96
  150. package/dist/templates/showcase/pages/lists/view.dml +0 -59
  151. package/dist/templates/showcase/pages/navigation/controller.ts +0 -64
  152. package/dist/templates/showcase/pages/navigation/state.ts +0 -17
  153. package/dist/templates/showcase/pages/navigation/view.dml +0 -34
  154. package/dist/templates/showcase/pages/network/controller.ts +0 -107
  155. package/dist/templates/showcase/pages/network/state.ts +0 -15
  156. package/dist/templates/showcase/pages/network/view.dml +0 -56
  157. package/dist/templates/showcase/pages/payments/controller.ts +0 -42
  158. package/dist/templates/showcase/pages/payments/view.dml +0 -35
  159. package/dist/templates/showcase/pages/state/view.dml +0 -33
  160. package/dist/templates/showcase/pages/text/controller.ts +0 -10
  161. package/dist/templates/showcase/pages/text/state.ts +0 -7
  162. package/dist/templates/showcase/pages/text/view.dml +0 -49
@@ -0,0 +1,445 @@
1
+ import type { OrderLineItem, PaymentMethod, PaymentOptions } from '@monimesl/dot-runtime';
2
+
3
+ // What the tester sets above the cases.
4
+ export interface CaseInput {
5
+ tickets: number;
6
+ phoneNumber: string;
7
+ financialAccountId: string;
8
+ }
9
+
10
+ export interface PaymentCase {
11
+ id: string;
12
+ title: string;
13
+ // What should happen, so the tester can tell a pass from a fail.
14
+ expect: string;
15
+ options: (input: CaseInput, reference: string) => PaymentOptions;
16
+ // Asks again with the last order's reference, as the same reference never pays twice.
17
+ reusesReference?: boolean;
18
+ // Calls cancel() this long after asking.
19
+ cancelAfterMilliseconds?: number;
20
+ }
21
+
22
+ export interface CaseGroup {
23
+ id: string;
24
+ title: string;
25
+ subtitle: string;
26
+ cases: PaymentCase[];
27
+ }
28
+
29
+ // SLE 0.50 a ticket, in minor units as Monime counts money, so trying a case with real money costs
30
+ // little: the two tickets the page starts with come to SLE 1.00.
31
+ const TICKET_PRICE = 50;
32
+ const SAMPLE_PHONE_NUMBER = '+23276123456';
33
+ const SAMPLE_ACCOUNT_ID = 'fac-k6VgVTfSmLruHpiQgMg4DUeySZx';
34
+ // httpbin.org is in network.hosts in dot.json, so Dash may download its pictures.
35
+ const PROGRAMME_PICTURE = 'https://httpbin.org/image/png';
36
+
37
+ function tickets(input: CaseInput, currency: 'SLE' | 'USD' = 'SLE'): OrderLineItem {
38
+ return {
39
+ name: 'Standing ticket',
40
+ unitPrice: { currency, value: TICKET_PRICE },
41
+ quantity: input.tickets,
42
+ };
43
+ }
44
+
45
+ function jazzNight(reference: string, lineItems: OrderLineItem[]): PaymentOptions {
46
+ return {
47
+ order: {
48
+ name: 'Jazz Night tickets',
49
+ reference,
50
+ description: 'Saturday 8 pm at the Bo Town Hall',
51
+ lineItems,
52
+ metadata: { show: 'jazz-night' },
53
+ },
54
+ };
55
+ }
56
+
57
+ function paying(methods: PaymentMethod[]) {
58
+ return (input: CaseInput, reference: string): PaymentOptions => ({
59
+ ...jazzNight(reference, [tickets(input)]),
60
+ methods,
61
+ });
62
+ }
63
+
64
+ function withItems(lineItems: OrderLineItem[]) {
65
+ return (_input: CaseInput, reference: string): PaymentOptions => jazzNight(reference, lineItems);
66
+ }
67
+
68
+ function changed(change: (options: PaymentOptions) => void) {
69
+ return (input: CaseInput, reference: string): PaymentOptions => {
70
+ const options = jazzNight(reference, [tickets(input)]);
71
+ change(options);
72
+ return options;
73
+ };
74
+ }
75
+
76
+ export const GROUPS: CaseGroup[] = [
77
+ {
78
+ id: 'methods',
79
+ title: 'Ways to pay',
80
+ subtitle: 'Which methods the sheet offers, in what order, and with which limits',
81
+ cases: [
82
+ {
83
+ id: 'any',
84
+ title: 'Any way',
85
+ expect: 'The sheet offers a linked account, a card and mobile money, in that order.',
86
+ options: (input, reference) => jazzNight(reference, [tickets(input)]),
87
+ },
88
+ {
89
+ id: 'card',
90
+ title: 'Card only',
91
+ expect: 'Only card. The result has a checkoutSessionId.',
92
+ options: paying([{ type: 'card' }]),
93
+ },
94
+ {
95
+ id: 'momo',
96
+ title: 'Mobile money only',
97
+ expect:
98
+ 'Only mobile money, with Orange Money and Africell Money. The result has a paymentCodeId.',
99
+ options: paying([{ type: 'momo' }]),
100
+ },
101
+ {
102
+ id: 'link',
103
+ title: 'Linked account only',
104
+ expect:
105
+ 'Only a linked account, greyed out if the person has none. The result has a paymentId.',
106
+ options: paying([{ type: 'link' }]),
107
+ },
108
+ {
109
+ id: 'reordered',
110
+ title: 'Mobile money, then card, then account',
111
+ expect: 'All three, in this order.',
112
+ options: paying([{ type: 'momo' }, { type: 'card' }, { type: 'link' }]),
113
+ },
114
+ {
115
+ id: 'orange',
116
+ title: 'Orange Money only',
117
+ expect: 'Mobile money with only Orange Money (m17).',
118
+ options: paying([{ type: 'momo', providers: { include: ['m17'] } }]),
119
+ },
120
+ {
121
+ id: 'africell',
122
+ title: 'Africell Money only',
123
+ expect: 'Mobile money with only Africell Money (m18).',
124
+ options: paying([{ type: 'momo', providers: { include: ['m18'] } }]),
125
+ },
126
+ {
127
+ id: 'one-number',
128
+ title: 'Mobile money from one number',
129
+ expect: 'Only the phone number above can pay. Monime refuses the code from any other.',
130
+ options: (input, reference) =>
131
+ paying([{ type: 'momo', phoneNumber: input.phoneNumber || SAMPLE_PHONE_NUMBER }])(
132
+ input,
133
+ reference,
134
+ ),
135
+ },
136
+ {
137
+ id: 'not-a-bank',
138
+ title: 'Account at any provider but one bank',
139
+ expect: 'Linked accounts, leaving out slb004.',
140
+ options: paying([{ type: 'link', providers: { exclude: ['slb004'] } }]),
141
+ },
142
+ {
143
+ id: 'banks',
144
+ title: 'Bank accounts only',
145
+ expect: 'Linked accounts at slb001, slb004 and slb007 only, so no mobile money wallets.',
146
+ options: paying([{ type: 'link', providers: { include: ['slb001', 'slb004', 'slb007'] } }]),
147
+ },
148
+ {
149
+ id: 'orange-or-account',
150
+ title: 'Orange Money from one number, or an account',
151
+ expect:
152
+ 'Mobile money limited to Orange Money and the number above, then linked accounts but slb004.',
153
+ options: (input, reference) =>
154
+ paying([
155
+ {
156
+ type: 'momo',
157
+ providers: { include: ['m17'] },
158
+ phoneNumber: input.phoneNumber || SAMPLE_PHONE_NUMBER,
159
+ },
160
+ { type: 'link', providers: { exclude: ['slb004'] } },
161
+ ])(input, reference),
162
+ },
163
+ ],
164
+ },
165
+ {
166
+ id: 'order',
167
+ title: 'The order',
168
+ subtitle: 'What the sheet shows and where the money goes',
169
+ cases: [
170
+ {
171
+ id: 'items',
172
+ title: 'Several items with a picture',
173
+ expect: 'Tickets, plus a SLE 0.10 programme with its picture. Dash adds up the total.',
174
+ options: (input, reference) =>
175
+ jazzNight(reference, [
176
+ tickets(input),
177
+ {
178
+ name: 'Programme',
179
+ unitPrice: { currency: 'SLE', value: 10 },
180
+ imageUrl: PROGRAMME_PICTURE,
181
+ },
182
+ ]),
183
+ },
184
+ {
185
+ id: 'plain',
186
+ title: 'No description or metadata',
187
+ expect: 'The sheet shows only the name, the items and the total.',
188
+ options: (input, reference) => ({
189
+ order: { name: 'Jazz Night tickets', reference, lineItems: [tickets(input)] },
190
+ }),
191
+ },
192
+ {
193
+ id: 'metadata',
194
+ title: 'Lots of metadata',
195
+ expect: "Pays as usual. The metadata stays with Monime's payment and its webhooks.",
196
+ options: changed((options) => {
197
+ options.order.metadata = {
198
+ show: 'jazz-night',
199
+ seats: 'A12,A13',
200
+ buyer: 'Fatmata Sesay',
201
+ town: 'Bo',
202
+ channel: 'showcase',
203
+ };
204
+ }),
205
+ },
206
+ {
207
+ id: 'usd',
208
+ title: 'In US dollars',
209
+ expect: 'USD 0.50 a ticket. Methods that only take SLE may be greyed out.',
210
+ options: (input, reference) => jazzNight(reference, [tickets(input, 'USD')]),
211
+ },
212
+ {
213
+ id: 'sixteen',
214
+ title: '16 items, the most an order takes',
215
+ expect: 'Sixteen songs at SLE 0.05 each, SLE 0.80 in all.',
216
+ options: withItems(
217
+ Array.from({ length: 16 }, (_, index) => ({
218
+ name: `Song ${index + 1}`,
219
+ unitPrice: { currency: 'SLE' as const, value: 5 },
220
+ })),
221
+ ),
222
+ },
223
+ {
224
+ id: 'account',
225
+ title: 'To a named account',
226
+ expect:
227
+ "The money goes to the financial account above. Dash refuses one that isn't the Space's, as invalid.",
228
+ options: (input, reference) => ({
229
+ ...jazzNight(reference, [tickets(input)]),
230
+ financialAccountId: input.financialAccountId || SAMPLE_ACCOUNT_ID,
231
+ }),
232
+ },
233
+ ],
234
+ },
235
+ {
236
+ id: 'endings',
237
+ title: 'References and endings',
238
+ subtitle: 'Paying twice, cancelling, and reading a payment back',
239
+ cases: [
240
+ {
241
+ id: 'same-reference',
242
+ title: 'The last order again',
243
+ expect:
244
+ 'Asks again with the last reference: the first payment comes back, and nobody pays twice.',
245
+ options: (input, reference) => jazzNight(reference, [tickets(input)]),
246
+ reusesReference: true,
247
+ },
248
+ {
249
+ id: 'cancel-soon',
250
+ title: 'Cancel after 5 seconds',
251
+ expect:
252
+ 'If no method is picked within 5 seconds, the sheet closes and request rejects as cancelled.',
253
+ options: (input, reference) => jazzNight(reference, [tickets(input)]),
254
+ cancelAfterMilliseconds: 5_000,
255
+ },
256
+ {
257
+ id: 'cancel-late',
258
+ title: 'Stop waiting after 20 seconds',
259
+ expect:
260
+ 'Pick a method in time: the payment carries on after the dot stops waiting. Check it with get.',
261
+ options: (input, reference) => jazzNight(reference, [tickets(input)]),
262
+ cancelAfterMilliseconds: 20_000,
263
+ },
264
+ ],
265
+ },
266
+ {
267
+ id: 'monime',
268
+ title: "Refused by Monime's rules",
269
+ subtitle:
270
+ 'Dash checks these with Monime before the sheet opens. The simulator only checks the shape.',
271
+ cases: [
272
+ {
273
+ id: 'below-smallest',
274
+ title: 'SLE 0.01',
275
+ expect:
276
+ "In Dash, rejects as invalid, naming Monime's smallest amount. The simulator opens the sheet.",
277
+ options: withItems([{ name: 'Sticker', unitPrice: { currency: 'SLE', value: 1 } }]),
278
+ },
279
+ {
280
+ id: 'above-largest',
281
+ title: 'SLE 100,000,000',
282
+ expect:
283
+ "In Dash, rejects as invalid, naming Monime's largest amount. The simulator opens the sheet.",
284
+ options: withItems([
285
+ { name: 'Stadium', unitPrice: { currency: 'SLE', value: 10_000_000_000 } },
286
+ ]),
287
+ },
288
+ {
289
+ id: 'other-account',
290
+ title: "An account that isn't the Space's",
291
+ expect: 'In Dash, rejects as invalid. The simulator takes any account shaped like fac-….',
292
+ options: changed((options) => {
293
+ options.financialAccountId = 'fac-NotThisSpacesAccount';
294
+ }),
295
+ },
296
+ ],
297
+ },
298
+ {
299
+ id: 'invalid',
300
+ title: 'Refused before Dash',
301
+ subtitle:
302
+ 'Every host checks these the same way, so each rejects as invalid with the same message.',
303
+ cases: [
304
+ {
305
+ id: 'no-items',
306
+ title: 'No line items',
307
+ expect: 'invalid: Give the order at least one line item',
308
+ options: withItems([]),
309
+ },
310
+ {
311
+ id: 'mixed-currencies',
312
+ title: 'Items in two currencies',
313
+ expect: "invalid: All of an order's line items must be in one currency",
314
+ options: (input, reference) =>
315
+ jazzNight(reference, [tickets(input), tickets(input, 'USD')]),
316
+ },
317
+ {
318
+ id: 'seventeen',
319
+ title: '17 items',
320
+ expect: 'invalid: An order can have at most 16 line items',
321
+ options: withItems(
322
+ Array.from({ length: 17 }, (_, index) => ({
323
+ name: `Song ${index + 1}`,
324
+ unitPrice: { currency: 'SLE' as const, value: 5 },
325
+ })),
326
+ ),
327
+ },
328
+ {
329
+ id: 'short-name',
330
+ title: 'A two-letter name',
331
+ expect: "invalid: An order's name needs 3 to 64 characters",
332
+ options: changed((options) => {
333
+ options.order.name = 'Jo';
334
+ }),
335
+ },
336
+ {
337
+ id: 'long-reference',
338
+ title: 'A 65-character reference',
339
+ expect: "invalid: An order's reference can be at most 64 characters",
340
+ options: (input) => jazzNight('r'.repeat(65), [tickets(input)]),
341
+ },
342
+ {
343
+ id: 'no-quantity',
344
+ title: 'A quantity of 0',
345
+ expect: "invalid: Line item 1's quantity must be a whole number from 1 to 100,000",
346
+ options: withItems([
347
+ {
348
+ name: 'Standing ticket',
349
+ unitPrice: { currency: 'SLE', value: TICKET_PRICE },
350
+ quantity: 0,
351
+ },
352
+ ]),
353
+ },
354
+ {
355
+ id: 'part-cent',
356
+ title: 'A price of 75.5 minor units',
357
+ expect: "invalid: Line item 1's price must be a whole number of minor units",
358
+ options: withItems([
359
+ { name: 'Standing ticket', unitPrice: { currency: 'SLE', value: 75.5 } },
360
+ ]),
361
+ },
362
+ {
363
+ id: 'other-host',
364
+ title: 'A picture from a host the dot never named',
365
+ expect:
366
+ "invalid: Line item 1's picture is on pictures.bo.sl, which isn't in network.hosts in dot.json",
367
+ options: withItems([
368
+ {
369
+ name: 'Standing ticket',
370
+ unitPrice: { currency: 'SLE', value: TICKET_PRICE },
371
+ imageUrl: 'https://pictures.bo.sl/ticket.png',
372
+ },
373
+ ]),
374
+ },
375
+ {
376
+ id: 'http-picture',
377
+ title: 'A picture over http',
378
+ expect: "invalid: Line item 1's picture must be an https link",
379
+ options: withItems([
380
+ {
381
+ name: 'Standing ticket',
382
+ unitPrice: { currency: 'SLE', value: TICKET_PRICE },
383
+ imageUrl: 'http://httpbin.org/image/png',
384
+ },
385
+ ]),
386
+ },
387
+ {
388
+ id: 'monime-key',
389
+ title: 'A metadata key starting with "monime"',
390
+ expect: 'invalid: The metadata key "monimeOrder" can\'t start with "@", "_" or "monime"',
391
+ options: changed((options) => {
392
+ options.order.metadata = { monimeOrder: '1042' };
393
+ }),
394
+ },
395
+ {
396
+ id: 'twice',
397
+ title: 'Card listed twice',
398
+ expect: 'invalid: The method "card" is listed twice',
399
+ options: paying([{ type: 'card' }, { type: 'card' }]),
400
+ },
401
+ {
402
+ id: 'both-filters',
403
+ title: 'Include and exclude together',
404
+ expect: "invalid: Give include or exclude for link's providers, not both",
405
+ options: paying([{ type: 'link', providers: { include: ['m17'], exclude: ['m18'] } }]),
406
+ },
407
+ {
408
+ id: 'unknown-provider',
409
+ title: "A provider Monime doesn't know",
410
+ expect: 'invalid: "m99" isn\'t a provider Monime knows for momo',
411
+ options: paying([{ type: 'momo', providers: { include: ['m99'] } }]),
412
+ },
413
+ {
414
+ id: 'local-number',
415
+ title: 'A number without +232',
416
+ expect: 'invalid: phoneNumber must be a full number starting with +',
417
+ options: paying([{ type: 'momo', phoneNumber: '076123456' }]),
418
+ },
419
+ {
420
+ id: 'bad-account',
421
+ title: "An account ID that isn't one",
422
+ expect: 'invalid: financialAccountId must be a Monime financial account ID',
423
+ options: changed((options) => {
424
+ options.financialAccountId = 'acc-1';
425
+ }),
426
+ },
427
+ {
428
+ id: 'no-methods',
429
+ title: 'An empty list of methods',
430
+ expect: 'invalid: Give at least one method, or leave methods out to offer all three',
431
+ options: paying([]),
432
+ },
433
+ ],
434
+ },
435
+ ];
436
+
437
+ export function findCase(id: string): PaymentCase | undefined {
438
+ for (const group of GROUPS) {
439
+ const found = group.cases.find((paymentCase) => paymentCase.id === id);
440
+ if (found !== undefined) {
441
+ return found;
442
+ }
443
+ }
444
+ return undefined;
445
+ }
@@ -0,0 +1,149 @@
1
+ import type { PaymentResult } from '@monimesl/dot-runtime';
2
+ import { errorMessage, isDashError } from '@monimesl/dot-runtime';
3
+
4
+ import { findCase } from './cases';
5
+ import type { Run } from './state';
6
+ import state from './state';
7
+
8
+ const MAX_RUNS = 20;
9
+ const NO_RESULT = {
10
+ method: '',
11
+ status: '',
12
+ amount: '',
13
+ resource: '',
14
+ requestId: '',
15
+ orderNumber: '',
16
+ };
17
+
18
+ export default definePage({
19
+ initialState: state,
20
+ configs: { navigationBar: { title: 'Payments' } },
21
+ mutations: {
22
+ setTickets: (_state, event: ValueEvent) => ({ tickets: Number(event.detail.value) }),
23
+ setPhoneNumber: (_state, event: ValueEvent) => ({ phoneNumber: event.detail.value.trim() }),
24
+ setFinancialAccountId: (_state, event: ValueEvent) => ({
25
+ financialAccountId: event.detail.value.trim(),
26
+ }),
27
+ },
28
+ actions: {
29
+ // data-case names the case to run, from cases.ts.
30
+ async run(state, event: DataEvent<{ case?: string }>) {
31
+ const paymentCase = findCase(event.data.case ?? '');
32
+ if (paymentCase === undefined) {
33
+ return;
34
+ }
35
+ // A new reference for each order, as the same reference never pays twice.
36
+ let reference = `tickets-${Date.now().toString(36)}`;
37
+ if (paymentCase.reusesReference && state.reference !== '') {
38
+ reference = state.reference;
39
+ }
40
+ this.setState({
41
+ ...NO_RESULT,
42
+ reference,
43
+ last: {
44
+ title: paymentCase.title,
45
+ expect: paymentCase.expect,
46
+ outcome: 'Waiting for the payment sheet',
47
+ },
48
+ });
49
+ const input = {
50
+ tickets: state.tickets,
51
+ phoneNumber: state.phoneNumber,
52
+ financialAccountId: state.financialAccountId,
53
+ };
54
+ const request = this.dash.payments.request(paymentCase.options(input, reference));
55
+ const cancelAfter = paymentCase.cancelAfterMilliseconds;
56
+ if (cancelAfter !== undefined) {
57
+ // Runs beside the payment. Cancelling one that already ended does nothing.
58
+ this.dash.system.delay(cancelAfter).then(
59
+ () => request.cancel(),
60
+ () => undefined,
61
+ );
62
+ }
63
+ let outcome: string;
64
+ try {
65
+ const result = await request;
66
+ outcome = resultOutcome(result);
67
+ this.setState(shownResult(result));
68
+ } catch (error) {
69
+ outcome = errorOutcome(error);
70
+ }
71
+ // Another run may have started meanwhile, so this one names itself again.
72
+ this.setState((current) => ({
73
+ last: { title: paymentCase.title, expect: paymentCase.expect, outcome },
74
+ runs: added(current.runs, paymentCase.title, outcome),
75
+ }));
76
+ },
77
+ async checkPayment(state) {
78
+ const title = 'Check the last payment';
79
+ const found = await lookedUp(this.dash.payments.get(state.reference));
80
+ this.setState((current) => ({
81
+ ...found.shown,
82
+ last: { title, expect: '', outcome: found.outcome },
83
+ runs: added(current.runs, title, found.outcome),
84
+ }));
85
+ },
86
+ async lookUpUnknown() {
87
+ const title = 'Look up an order that never was';
88
+ const found = await lookedUp(this.dash.payments.get('order-that-never-was'));
89
+ this.setState((current) => ({
90
+ last: {
91
+ title,
92
+ expect: 'undefined: there is no payment for this reference',
93
+ outcome: found.outcome,
94
+ },
95
+ runs: added(current.runs, title, found.outcome),
96
+ }));
97
+ },
98
+ },
99
+ });
100
+
101
+ async function lookedUp(lookup: Promise<PaymentResult | undefined>) {
102
+ try {
103
+ const result = await lookup;
104
+ if (result === undefined) {
105
+ return { outcome: 'undefined: there is no payment for this reference', shown: {} };
106
+ }
107
+ return { outcome: resultOutcome(result), shown: shownResult(result) };
108
+ } catch (error) {
109
+ return { outcome: errorOutcome(error), shown: {} };
110
+ }
111
+ }
112
+
113
+ function resultOutcome(result: PaymentResult): string {
114
+ return `${result.status} by ${result.method}, ${moneyText(result.amount)}`;
115
+ }
116
+
117
+ function errorOutcome(error: unknown): string {
118
+ if (isDashError(error)) {
119
+ return `${error.code}: ${error.message}`;
120
+ }
121
+ return errorMessage(error);
122
+ }
123
+
124
+ function moneyText(money: PaymentResult['amount']): string {
125
+ return `${money.currency} ${(money.value / 100).toFixed(2)}`;
126
+ }
127
+
128
+ // The Monime resource behind the payment differs by method.
129
+ function shownResult(result: PaymentResult) {
130
+ let resource = result.paymentId ?? '';
131
+ if (result.method === 'card') {
132
+ resource = result.checkoutSessionId;
133
+ } else if (result.method === 'momo') {
134
+ resource = result.paymentCodeId;
135
+ }
136
+ return {
137
+ method: result.method,
138
+ status: result.status,
139
+ amount: moneyText(result.amount),
140
+ resource,
141
+ requestId: result.requestId,
142
+ orderNumber: result.orderNumber ?? '',
143
+ };
144
+ }
145
+
146
+ function added(runs: readonly Run[], title: string, outcome: string): Run[] {
147
+ const run = { key: `${Date.now()}-${runs.length}`, title, outcome };
148
+ return [run, ...runs].slice(0, MAX_RUNS);
149
+ }
@@ -0,0 +1,33 @@
1
+ import { GROUPS } from './cases';
2
+
3
+ export interface Run {
4
+ key: string;
5
+ title: string;
6
+ outcome: string;
7
+ }
8
+
9
+ export default {
10
+ // The cases as the view lists them, without the functions that build their requests.
11
+ groups: GROUPS.map((group) => ({
12
+ id: group.id,
13
+ title: group.title,
14
+ subtitle: group.subtitle,
15
+ cases: group.cases.map((paymentCase) => ({
16
+ id: paymentCase.id,
17
+ title: paymentCase.title,
18
+ expect: paymentCase.expect,
19
+ })),
20
+ })),
21
+ tickets: 2,
22
+ phoneNumber: '',
23
+ financialAccountId: '',
24
+ reference: '',
25
+ last: { title: '', expect: '', outcome: '' },
26
+ method: '',
27
+ status: '',
28
+ amount: '',
29
+ resource: '',
30
+ requestId: '',
31
+ orderNumber: '',
32
+ runs: [] as Run[],
33
+ };
@@ -0,0 +1,71 @@
1
+ <View>
2
+ <Text variant="title">dash.payments</Text>
3
+ <Text>Takes a payment for an order through Dash's own payment sheet. The person picks a linked account, a card or mobile money, and the money goes to the Space that owns the dot. It needs the payments permission in dot.json. Each case below says what should happen. In Dash the money is real, so the orders cost a few cents.</Text>
4
+
5
+ <View gap="small">
6
+ <Text variant="subtitle">The order</Text>
7
+ <Text variant="caption" tone="muted">The cases use these. Leave a field empty for a sample.</Text>
8
+ <View variant="card" gap="medium">
9
+ <Stepper label="Tickets" min="1" max="10" hint="SLE 0.50 each" value="{{ tickets }}" onChange="setTickets" />
10
+ <Input label="Phone number that may pay" placeholder="+23276123456" value="{{ phoneNumber }}" maxLength="16" onChange="setPhoneNumber" />
11
+ <Input label="Financial account" placeholder="fac-k6VgVTfSmLruHpiQgMg4DUeySZx" value="{{ financialAccountId }}" maxLength="64" onChange="setFinancialAccountId" />
12
+ </View>
13
+ </View>
14
+
15
+ <View gap="small">
16
+ <Text variant="subtitle">request</Text>
17
+ <Text variant="caption" tone="muted">Tap a case to ask Dash for that payment. Each case gets a new reference, except The last order again.</Text>
18
+ <For each="{{ groups }}" item="group" key="id">
19
+ <Disclosure title="{{ group.title }}" subtitle="{{ group.subtitle }}" open="{{ group.id === 'methods' }}">
20
+ <List>
21
+ <For each="{{ group.cases }}" item="paymentCase" key="id">
22
+ <ListItem title="{{ paymentCase.title }}" subtitle="{{ paymentCase.expect }}" data-case="{{ paymentCase.id }}" onTap="run" />
23
+ </For>
24
+ </List>
25
+ </Disclosure>
26
+ </For>
27
+ </View>
28
+
29
+ <If condition="{{ last.title }}">
30
+ <View gap="small">
31
+ <Text variant="subtitle">{{ last.title }}</Text>
32
+ <If condition="{{ last.expect }}">
33
+ <Text variant="caption" tone="muted">Expected: {{ last.expect }}</Text>
34
+ </If>
35
+ <Notice>{{ last.outcome }}</Notice>
36
+ <If condition="{{ status }}">
37
+ <View variant="card" gap="none">
38
+ <DetailRow label="reference" value="{{ reference }}" />
39
+ <DetailRow label="method" value="{{ method }}" />
40
+ <DetailRow label="status" value="{{ status }}" />
41
+ <DetailRow label="amount" value="{{ amount }}" />
42
+ <DetailRow label="resource" value="{{ resource }}" />
43
+ <DetailRow label="requestId" value="{{ requestId }}" />
44
+ <If condition="{{ orderNumber }}">
45
+ <DetailRow label="orderNumber" value="{{ orderNumber }}" />
46
+ </If>
47
+ </View>
48
+ </If>
49
+ </View>
50
+ </If>
51
+
52
+ <View gap="small">
53
+ <Text variant="subtitle">get</Text>
54
+ <Text variant="caption" tone="muted">Reads a payment back by the order's reference, such as one still pending, or after the dot opens again. The resource is the Monime resource behind the payment: a checkout session for a card, a payment code for mobile money, or the payment itself for a linked account.</Text>
55
+ <View direction="row" wrap gap="small">
56
+ <Button variant="secondary" onClick="checkPayment" disabled="{{ !reference }}">Check the last payment</Button>
57
+ <Button variant="secondary" onClick="lookUpUnknown">Look up an order that never was</Button>
58
+ </View>
59
+ </View>
60
+
61
+ <If condition="{{ runs.length }}">
62
+ <View gap="small">
63
+ <Text variant="subtitle">Runs</Text>
64
+ <List>
65
+ <For each="{{ runs }}" item="run" key="key">
66
+ <ListItem title="{{ run.title }}" subtitle="{{ run.outcome }}" />
67
+ </For>
68
+ </List>
69
+ </View>
70
+ </If>
71
+ </View>