cabloy 5.1.141 → 5.1.142

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 (92) hide show
  1. package/.cabloy-version +1 -1
  2. package/.github/workflows/playwright-e2e.yml +2 -2
  3. package/CHANGELOG.md +21 -0
  4. package/cabloy-docs/frontend/form-guide.md +47 -0
  5. package/cabloy-docs/frontend/markdown-guide.md +50 -17
  6. package/cabloy-docs/frontend/route-alias-guide.md +20 -0
  7. package/cabloy-docs/frontend/scripts.md +9 -11
  8. package/cabloy-docs/frontend/zova-form-under-the-hood.md +25 -0
  9. package/cabloy-docs/fullstack/parallel-worktree-environment.md +1 -1
  10. package/cabloy-docs/fullstack/quickstart.md +5 -4
  11. package/cabloy-docs/reference/repo-scripts.md +29 -42
  12. package/e2e/config/playwright.config.ts +34 -0
  13. package/e2e/scripts/e2e.ts +2 -27
  14. package/e2e/scripts/runE2e.ts +62 -0
  15. package/e2e/scripts/runE2eArgs.test.ts +75 -0
  16. package/e2e/scripts/runE2eArgs.ts +158 -0
  17. package/e2e/specs/{a-commerce/commerce.spec.ts → a-commerce.spec.ts} +361 -30
  18. package/e2e/specs/{cabloy-basic/account.spec.ts → account.spec.ts} +1 -2
  19. package/oxlint.config.ts +3 -0
  20. package/package.json +3 -9
  21. package/scripts/upgrade.ts +63 -68
  22. package/vona/packages-utils/lint/package.json +1 -1
  23. package/vona/packages-utils/lint/src/oxc/lint.ts +1 -1
  24. package/vona/packages-utils/lint/src/oxc/lintVue.ts +1 -1
  25. package/vona/pnpm-lock.yaml +41 -41
  26. package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/refundRecoveryView.tsx +7 -2
  27. package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/order.ts +31 -15
  28. package/vona/src/suite/a-home/modules/home-user/package.json +1 -0
  29. package/vona/src/suite/a-home/modules/home-user/src/service/account.ts +15 -2
  30. package/vona/src/suite/a-home/modules/home-user/test/accountActivation.test.ts +34 -4
  31. package/vona/src/suite-vendor/a-cabloy/modules/a-markdown/package.json +2 -1
  32. package/vona/src/suite-vendor/a-cabloy/modules/a-markdown/src/.metadata/index.ts +26 -0
  33. package/vona/src/suite-vendor/a-cabloy/modules/a-markdown/src/bean/imageScene.markdown.ts +13 -0
  34. package/vona/src/suite-vendor/a-cabloy/package.json +2 -2
  35. package/vona/src/suite-vendor/a-pay/modules/a-pay/package.json +1 -1
  36. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/providerOperation.ts +23 -0
  37. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/refundOperation.ts +7 -0
  38. package/vona/src/suite-vendor/a-pay/modules/pay-mock/package.json +1 -1
  39. package/vona/src/suite-vendor/a-pay/modules/pay-paypal/package.json +1 -1
  40. package/vona/src/suite-vendor/a-pay/modules/pay-stripe/package.json +1 -1
  41. package/vona/src/suite-vendor/a-pay/package.json +5 -5
  42. package/zova/packages-utils/zova-jsx/package.json +2 -2
  43. package/zova/packages-zova/zova/package.json +3 -3
  44. package/zova/packages-zova/zova-core/package.json +1 -1
  45. package/zova/packages-zova/zova-core/src/bean/resource/locale/type.ts +1 -0
  46. package/zova/pnpm-lock.yaml +63 -18
  47. package/zova/src/front/config/config/config.cabloyBasicWeb.ts +3 -1
  48. package/zova/src/front/config/config/config.cabloyCommerce.ts +4 -2
  49. package/zova/src/suite/a-commerce/modules/commerce-member/src/page/address/controller.tsx +64 -63
  50. package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/types.ts +6 -1
  51. package/zova/src/suite/a-commerce/modules/commerce-trade/src/component/tableCellActionRefund/controller.tsx +413 -134
  52. package/zova/src/suite/a-commerce/modules/commerce-trade/src/config/locale/en-us.ts +76 -0
  53. package/zova/src/suite/a-commerce/modules/commerce-trade/src/config/locale/zh-cn.ts +67 -0
  54. package/zova/src/suite/a-commerce/modules/commerce-trade/src/model/order.ts +1 -0
  55. package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/cart/controller.tsx +1 -1
  56. package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/checkout/controller.tsx +7 -7
  57. package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/order/controller.tsx +46 -9
  58. package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/payment/controller.tsx +50 -30
  59. package/zova/src/suite/cabloy-basic/modules/basic-captcha/src/component/formFieldCaptcha/controller.tsx +6 -2
  60. package/zova/src/suite/cabloy-basic/modules/basic-image/src/component/formFieldImage/controller.tsx +9 -15
  61. package/zova/src/suite/cabloy-basic/modules/basic-markdown/package.json +10 -2
  62. package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/.metadata/index.ts +11 -2
  63. package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/.metadata/locales.ts +7 -0
  64. package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/component/formFieldMarkdown/controller.tsx +723 -5
  65. package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/component/formFieldMarkdown/render.tsx +461 -2
  66. package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/config/locale/en-us.ts +59 -0
  67. package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/config/locale/zh-cn.ts +59 -0
  68. package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/index.ts +1 -0
  69. package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/lib/codeBlockLanguages.ts +42 -0
  70. package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/lib/richTextContentStyle.ts +20 -0
  71. package/zova/src/suite/cabloy-basic/modules/basic-markdown/test/lib/codeBlockLanguages.test.ts +25 -0
  72. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/package.json +1 -1
  73. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/model/resource.ts +1 -0
  74. package/zova/src/suite-vendor/a-cabloy/package.json +2 -2
  75. package/zova/src/suite-vendor/a-pay/modules/a-pay/package.json +1 -1
  76. package/zova/src/suite-vendor/a-pay/modules/a-pay/src/component/paymentNextAction/controller.tsx +26 -12
  77. package/zova/src/suite-vendor/a-pay/modules/pay-mock/cli/openapi.config.ts +1 -1
  78. package/zova/src/suite-vendor/a-pay/modules/pay-mock/package.json +1 -1
  79. package/zova/src/suite-vendor/a-pay/modules/pay-mock/src/api/payMockMockPayment.ts +26 -0
  80. package/zova/src/suite-vendor/a-pay/modules/pay-mock/src/apiSchema/payMockMockPayment.ts +8 -1
  81. package/zova/src/suite-vendor/a-pay/modules/pay-mock/src/model/payMockPayment.ts +13 -1
  82. package/zova/src/suite-vendor/a-pay/package.json +3 -3
  83. package/zova/src/suite-vendor/a-zova/modules/a-form/package.json +1 -1
  84. package/zova/src/suite-vendor/a-zova/modules/a-form/src/component/formField/controller.tsx +2 -0
  85. package/zova/src/suite-vendor/a-zova/modules/a-form/src/types/formField.ts +7 -6
  86. package/zova/src/suite-vendor/a-zova/modules/a-zova/package.json +2 -2
  87. package/zova/src/suite-vendor/a-zova/package.json +3 -3
  88. package/e2e/config/playwright.basic.config.ts +0 -3
  89. package/e2e/config/playwright.commerce.config.ts +0 -3
  90. package/e2e/config/playwright.shared.config.ts +0 -38
  91. package/e2e/scripts/testE2eClean.ts +0 -56
  92. /package/e2e/specs/{cabloy-basic/basic.spec.ts → cabloy-basic.spec.ts} +0 -0
@@ -1,5 +1,6 @@
1
1
  import type { TableIdentity } from 'table-identity';
2
2
  import type { IJsxRenderContextTableCell } from 'zova-module-a-table';
3
+ import type { ModelPayMockPayment } from 'zova-module-pay-mock';
3
4
 
4
5
  import { BeanControllerBase, Use } from 'zova';
5
6
  import { Controller } from 'zova-module-a-bean';
@@ -23,10 +24,18 @@ export class ControllerTableCellActionRefund extends BeanControllerBase {
23
24
  @Use({ beanFullName: 'commerce-trade.model.order' })
24
25
  $$modelOrder: ModelOrder;
25
26
 
26
- reason = '';
27
- confirmed = false;
27
+ @Use({ beanFullName: 'pay-mock.model.payMockPayment' })
28
+ $$modelPayMockPayment: ModelPayMockPayment;
29
+
30
+ decisionReason = '';
31
+ decisionConfirmed = false;
32
+ executionConfirmed = false;
28
33
  recoveryOpen = false;
29
34
  recoveryLoaded = false;
35
+ recoveryUnavailable = false;
36
+ recoveryError?: unknown;
37
+ mockCompletionError?: unknown;
38
+ mockCompletionSucceeded = false;
30
39
  recoveryReason = '';
31
40
  retryRiskAcknowledged = false;
32
41
  approveIdempotencyKey?: string;
@@ -34,185 +43,455 @@ export class ControllerTableCellActionRefund extends BeanControllerBase {
34
43
  reconcileIdempotencyKey?: string;
35
44
  retryIdempotencyKey?: string;
36
45
 
37
- private async _approve() {
38
- const reason = this.reason.trim();
39
- if (!reason || !this.confirmed) return;
40
- const orderId = this.$$renderContext.cellContext.row.id as TableIdentity;
41
- const idempotencyKey = this.approveIdempotencyKey ?? crypto.randomUUID();
42
- this.approveIdempotencyKey = idempotencyKey;
43
- await this.$$modelOrder.approveRefund(orderId).mutateAsync({ reason, idempotencyKey });
44
- this.approveIdempotencyKey = undefined;
46
+ private get _orderId() {
47
+ return this.$$renderContext.cellContext.row.id as TableIdentity;
48
+ }
49
+
50
+ private get _decisionPending() {
51
+ return (
52
+ this.$$modelOrder.approveRefund(this._orderId).isPending ||
53
+ this.$$modelOrder.rejectRefund(this._orderId).isPending
54
+ );
55
+ }
56
+
57
+ private get _decisionValid() {
58
+ return !!this.decisionReason.trim() && this.decisionConfirmed;
59
+ }
60
+
61
+ private async _submitDecision(approved: boolean, close: () => void) {
62
+ const reason = this.decisionReason.trim();
63
+ if (!reason || !this.decisionConfirmed || this._decisionPending) return;
64
+ const idempotencyKey = approved
65
+ ? (this.approveIdempotencyKey ?? crypto.randomUUID())
66
+ : (this.rejectIdempotencyKey ?? crypto.randomUUID());
67
+ if (approved) {
68
+ this.approveIdempotencyKey = idempotencyKey;
69
+ } else {
70
+ this.rejectIdempotencyKey = idempotencyKey;
71
+ }
72
+ const mutation = approved
73
+ ? this.$$modelOrder.approveRefund(this._orderId)
74
+ : this.$$modelOrder.rejectRefund(this._orderId);
75
+ await mutation.mutateAsync({ reason, idempotencyKey });
76
+ if (approved) {
77
+ this.approveIdempotencyKey = undefined;
78
+ } else {
79
+ this.rejectIdempotencyKey = undefined;
80
+ }
81
+ close();
45
82
  }
46
83
 
47
- private async _reject() {
48
- const reason = this.reason.trim();
49
- if (!reason || !this.confirmed) return;
50
- const orderId = this.$$renderContext.cellContext.row.id as TableIdentity;
51
- const idempotencyKey = this.rejectIdempotencyKey ?? crypto.randomUUID();
52
- this.rejectIdempotencyKey = idempotencyKey;
53
- await this.$$modelOrder.rejectRefund(orderId).mutateAsync({ reason, idempotencyKey });
84
+ private _openRefundDecisionDialog() {
85
+ this.decisionReason = '';
86
+ this.decisionConfirmed = false;
87
+ this.approveIdempotencyKey = undefined;
54
88
  this.rejectIdempotencyKey = undefined;
89
+ this.$appModal.dialog(
90
+ {
91
+ title: this.scope.locale.RefundDecision(),
92
+ slotDefault: () => {
93
+ return (
94
+ <>
95
+ <textarea
96
+ class="textarea textarea-bordered w-full"
97
+ name="decisionReason"
98
+ placeholder={this.scope.locale.RefundDecisionReason()}
99
+ v-model={this.decisionReason}
100
+ />
101
+ <label class="label cursor-pointer mt-2 justify-start gap-2">
102
+ <input
103
+ class="checkbox checkbox-sm"
104
+ name="decisionConfirmed"
105
+ type="checkbox"
106
+ v-model={this.decisionConfirmed}
107
+ />
108
+ <span class="label-text text-xs">{this.scope.locale.Confirm()}</span>
109
+ </label>
110
+ </>
111
+ );
112
+ },
113
+ slotActions: dialog => {
114
+ const disabled = !this._decisionValid || this._decisionPending;
115
+ return (
116
+ <>
117
+ {this._decisionPending && <span class="loading loading-spinner text-primary"></span>}
118
+ <button class="btn btn-ghost" type="button" onClick={dialog.close}>
119
+ {this.scope.locale.Cancel()}
120
+ </button>
121
+ <button
122
+ class="btn btn-error"
123
+ type="button"
124
+ disabled={disabled}
125
+ onClick={() => this._submitDecision(false, dialog.close)}
126
+ >
127
+ {this.scope.locale.RejectRefund()}
128
+ </button>
129
+ <button
130
+ class="btn btn-warning"
131
+ type="button"
132
+ disabled={disabled}
133
+ onClick={() => this._submitDecision(true, dialog.close)}
134
+ >
135
+ {this.scope.locale.ApproveRefund()}
136
+ </button>
137
+ </>
138
+ );
139
+ },
140
+ },
141
+ {
142
+ closeOnBackdrop: false,
143
+ closeOnEscape: false,
144
+ maxWidth: 480,
145
+ showCloseButton: true,
146
+ },
147
+ );
148
+ }
149
+
150
+ private get _executionPending() {
151
+ return this.$$modelOrder.executeRefund(this._orderId).isPending;
152
+ }
153
+
154
+ private get _recoveryActionPending() {
155
+ return (
156
+ this.$$modelOrder.reconcileRefund(this._orderId).isPending ||
157
+ this.$$modelOrder.retryRefund(this._orderId).isPending
158
+ );
159
+ }
160
+
161
+ private get _mockCompletion() {
162
+ const recovery = this._recovery().data as
163
+ | ApiSchemaCommerceTradeDtoRefundRecoveryView
164
+ | undefined;
165
+ if (!recovery || recovery.providerName !== 'pay-mock:mock') return undefined;
166
+ if (recovery.refundOperationState !== 'pending') return undefined;
167
+ return this.$$modelPayMockPayment.completeRefund(recovery.refundOperationId);
55
168
  }
56
169
 
57
- private async _execute() {
58
- if (!this.confirmed) return;
59
- const orderId = this.$$renderContext.cellContext.row.id as TableIdentity;
60
- await this.$$modelOrder.executeRefund(orderId).mutateAsync(undefined);
170
+ private get _mockCompletionAvailable() {
171
+ return (
172
+ this.sys.env.META_MODE !== 'production' &&
173
+ this.recoveryOpen &&
174
+ this.recoveryDisposition === 'await_webhook'
175
+ );
176
+ }
177
+
178
+ private get recoveryDisposition() {
179
+ const recovery = this._recovery().data as
180
+ | ApiSchemaCommerceTradeDtoRefundRecoveryView
181
+ | undefined;
182
+ return recovery?.recoveryDisposition;
183
+ }
184
+
185
+ private async _completeMockRefund() {
186
+ const mutation = this._mockCompletion;
187
+ if (!mutation || mutation.isPending) return;
188
+ this.mockCompletionError = undefined;
189
+ try {
190
+ await mutation.mutateAsync({ outcome: 'succeeded' });
191
+ this.mockCompletionSucceeded = true;
192
+ this.recoveryError = undefined;
193
+ this.recoveryUnavailable = false;
194
+ this.recoveryLoaded = true;
195
+ await this.$$modelOrder.select()?.refetch();
196
+ } catch (error) {
197
+ this.mockCompletionError = error;
198
+ }
199
+ }
200
+
201
+ private async _execute(close: () => void) {
202
+ if (!this.executionConfirmed || this._executionPending) return;
203
+ await this.$$modelOrder.executeRefund(this._orderId).mutateAsync(undefined);
204
+ close();
61
205
  }
62
206
 
63
207
  private _recovery() {
64
- const orderId = this.$$renderContext.cellContext.row.id as TableIdentity;
208
+ const orderId = this._orderId;
65
209
  return this.$$modelOrder.refundRecovery(orderId);
66
210
  }
67
211
 
212
+ private _isRecoveryUnavailable(error: unknown) {
213
+ const candidate = error as { code?: unknown; message?: unknown; status?: unknown } | undefined;
214
+ const status = candidate?.code ?? candidate?.status;
215
+ return status === 404 && candidate?.message === 'refund recovery is not available';
216
+ }
217
+
218
+ private async _loadRecovery() {
219
+ this.recoveryError = undefined;
220
+ this.recoveryUnavailable = false;
221
+ const result = await this._recovery().refetch();
222
+ if (result.error) {
223
+ if (this._isRecoveryUnavailable(result.error)) {
224
+ this.recoveryUnavailable = true;
225
+ this.recoveryLoaded = true;
226
+ } else {
227
+ this.recoveryError = result.error;
228
+ this.recoveryLoaded = false;
229
+ }
230
+ return;
231
+ }
232
+ this.recoveryLoaded = true;
233
+ }
234
+
68
235
  private async _toggleRecovery() {
69
236
  this.recoveryOpen = !this.recoveryOpen;
70
237
  if (this.recoveryOpen && !this.recoveryLoaded) {
71
- await this._recovery().refetch();
72
- this.recoveryLoaded = true;
238
+ await this._loadRecovery();
73
239
  }
74
240
  }
75
241
 
76
242
  private async _reconcile() {
77
243
  const reason = this.recoveryReason.trim();
78
- if (!reason) return;
79
- const orderId = this.$$renderContext.cellContext.row.id as TableIdentity;
244
+ if (!reason || this._recoveryActionPending) return;
245
+ const orderId = this._orderId;
80
246
  const actionIdempotencyKey = this.reconcileIdempotencyKey ?? crypto.randomUUID();
81
247
  this.reconcileIdempotencyKey = actionIdempotencyKey;
82
248
  await this.$$modelOrder
83
249
  .reconcileRefund(orderId)
84
250
  .mutateAsync({ reason, actionIdempotencyKey, acknowledgeRetryRisk: false });
85
251
  this.reconcileIdempotencyKey = undefined;
86
- await this._recovery().refetch();
252
+ await this._loadRecovery();
87
253
  }
88
254
 
89
255
  private async _retry() {
90
256
  const reason = this.recoveryReason.trim();
91
- if (!reason || !this.retryRiskAcknowledged) return;
92
- const orderId = this.$$renderContext.cellContext.row.id as TableIdentity;
257
+ if (!reason || !this.retryRiskAcknowledged || this._recoveryActionPending) return;
258
+ const orderId = this._orderId;
93
259
  const actionIdempotencyKey = this.retryIdempotencyKey ?? crypto.randomUUID();
94
260
  this.retryIdempotencyKey = actionIdempotencyKey;
95
261
  await this.$$modelOrder
96
262
  .retryRefund(orderId)
97
263
  .mutateAsync({ reason, actionIdempotencyKey, acknowledgeRetryRisk: true });
98
264
  this.retryIdempotencyKey = undefined;
99
- await this._recovery().refetch();
265
+ await this._loadRecovery();
100
266
  }
101
267
 
102
- protected render() {
103
- const props = this.$props as ControllerTableCellActionRefundProps;
104
- const order = this.$$renderContext.cellContext.row
105
- .original as ApiSchemaCommerceTradeDtoOrderSelectResItem;
106
- const requested = order.state === 'refund_requested';
107
- const recovery = this.recoveryOpen ? this._recovery() : undefined;
108
- const recoveryData = recovery?.data as ApiSchemaCommerceTradeDtoRefundRecoveryView | undefined;
109
- const showExecute = !recoveryData;
110
- return (
111
- <div class="flex flex-col items-start gap-2">
112
- <div class="join">
113
- {requested && (
114
- <input
115
- class="input input-bordered input-sm join-item w-40"
116
- name="reason"
117
- placeholder={this.scope.locale.RefundDecisionReason()}
118
- v-model={this.reason}
119
- />
120
- )}
121
- <label class="label cursor-pointer join-item gap-1 px-2">
122
- <input
123
- class="checkbox checkbox-sm"
124
- name="confirmed"
125
- type="checkbox"
126
- v-model={this.confirmed}
127
- />
128
- <span class="label-text text-xs">{this.scope.locale.Confirm()}</span>
129
- </label>
130
- {requested ? (
131
- <>
132
- <button class={props.class} type="button" onClick={() => this._approve()}>
133
- {this.scope.locale.ApproveRefund()}
134
- </button>
135
- <button
136
- class="btn btn-outline btn-error join-item"
137
- type="button"
138
- onClick={() => this._reject()}
139
- >
140
- {this.scope.locale.RejectRefund()}
141
- </button>
142
- </>
143
- ) : (
144
- <>
145
- {showExecute && (
146
- <button class={props.class} type="button" onClick={() => this._execute()}>
147
- {this.scope.locale.ExecuteRefund()}
148
- </button>
149
- )}
268
+ private _resetExecutionDialogState() {
269
+ this.executionConfirmed = false;
270
+ this.mockCompletionError = undefined;
271
+ this.mockCompletionSucceeded = false;
272
+ this.recoveryOpen = false;
273
+ this.recoveryLoaded = false;
274
+ this.recoveryUnavailable = false;
275
+ this.recoveryError = undefined;
276
+ this.recoveryReason = '';
277
+ this.retryRiskAcknowledged = false;
278
+ this.reconcileIdempotencyKey = undefined;
279
+ this.retryIdempotencyKey = undefined;
280
+ }
281
+
282
+ private _openRefundExecutionDialog() {
283
+ this._resetExecutionDialogState();
284
+ this.$appModal.dialog(
285
+ {
286
+ title: this.scope.locale.RefundExecution(),
287
+ slotDefault: () => {
288
+ const recovery = this.recoveryOpen ? this._recovery() : undefined;
289
+ const recoveryData = recovery?.data as
290
+ | ApiSchemaCommerceTradeDtoRefundRecoveryView
291
+ | undefined;
292
+ return (
293
+ <div class="flex flex-col items-start gap-3">
294
+ <label class="label m-0 cursor-pointer justify-start gap-2 p-0">
295
+ <input
296
+ class="checkbox checkbox-sm"
297
+ name="executionConfirmed"
298
+ type="checkbox"
299
+ v-model={this.executionConfirmed}
300
+ />
301
+ <span class="label-text">{this.scope.locale.Confirm()}</span>
302
+ </label>
150
303
  <button
151
- class="btn btn-outline join-item"
304
+ class="btn btn-outline btn-sm"
152
305
  type="button"
153
306
  onClick={() => this._toggleRecovery()}
154
307
  >
155
308
  {this.scope.locale.InspectRefundRecovery()}
156
309
  </button>
157
- </>
158
- )}
159
- </div>
160
- {recoveryData && (
161
- <section class="w-full rounded border border-base-300 p-3 text-sm">
162
- <p>
163
- {this.scope.locale.RefundRecoveryState({
164
- state: recoveryData.providerOperationState,
165
- attemptCount: recoveryData.attemptCount,
166
- })}
167
- </p>
168
- {recoveryData.providerRefundId && (
169
- <p>
170
- {this.scope.locale.RefundRecoveryProviderRefundId({
171
- providerRefundId: recoveryData.providerRefundId,
172
- })}
173
- </p>
174
- )}
175
- {recoveryData.errorSummary && <p>{recoveryData.errorSummary}</p>}
176
- <p class="text-base-content/70">{recoveryData.recoveryMessage}</p>
177
- {recoveryData.recoveryDisposition !== 'none' && (
178
- <input
179
- class="input input-bordered input-sm mt-2 w-full"
180
- name="recoveryReason"
181
- placeholder={this.scope.locale.RefundRecoveryReason()}
182
- v-model={this.recoveryReason}
183
- />
184
- )}
185
- {(recoveryData.recoveryDisposition === 'query_only' ||
186
- recoveryData.recoveryDisposition === 'reconcile_only') && (
310
+ {this.recoveryOpen && (
311
+ <section class="w-full rounded border border-base-300 p-3 text-sm">
312
+ {recovery?.isFetching && !recoveryData && (
313
+ <div class="flex items-center gap-2">
314
+ <span class="loading loading-spinner text-primary"></span>
315
+ <span>{this.scope.locale.LoadingRefundRecovery()}</span>
316
+ </div>
317
+ )}
318
+ {this.recoveryUnavailable && (
319
+ <div role="alert" class="alert alert-info">
320
+ <span>{this.scope.locale.RefundRecoveryUnavailable()}</span>
321
+ </div>
322
+ )}
323
+ {this.mockCompletionSucceeded && (
324
+ <div role="status" class="alert alert-success">
325
+ <span>{this.scope.locale.MockRefundCompleted()}</span>
326
+ </div>
327
+ )}
328
+ {this.recoveryError && !this.mockCompletionSucceeded && (
329
+ <div role="alert" class="alert alert-error">
330
+ <span>{this.scope.locale.RefundRecoveryLoadFailed()}</span>
331
+ <button
332
+ class="btn btn-outline btn-sm"
333
+ type="button"
334
+ onClick={() => this._loadRecovery()}
335
+ >
336
+ {this.scope.locale.RetryInspectRefundRecovery()}
337
+ </button>
338
+ </div>
339
+ )}
340
+ {recoveryData &&
341
+ !this.mockCompletionSucceeded &&
342
+ !this.recoveryUnavailable &&
343
+ !this.recoveryError && (
344
+ <>
345
+ <p>
346
+ {this.scope.locale.RefundRecoveryState({
347
+ state: recoveryData.providerOperationState,
348
+ attemptCount: recoveryData.attemptCount,
349
+ })}
350
+ </p>
351
+ {recoveryData.providerRefundId && (
352
+ <p>
353
+ {this.scope.locale.RefundRecoveryProviderRefundId({
354
+ providerRefundId: recoveryData.providerRefundId,
355
+ })}
356
+ </p>
357
+ )}
358
+ {recoveryData.errorSummary && <p>{recoveryData.errorSummary}</p>}
359
+ <p class="text-base-content/70">{recoveryData.recoveryMessage}</p>
360
+ {recoveryData.recoveryDisposition === 'await_webhook' && (
361
+ <div class="mt-2 w-full rounded border border-info/40 bg-info/10 p-3">
362
+ <p>{this.scope.locale.RefundAwaitingProviderConfirmation()}</p>
363
+ {recoveryData.providerName === 'pay-mock:mock' &&
364
+ this._mockCompletionAvailable && (
365
+ <>
366
+ <p class="mt-1 text-base-content/70">
367
+ {this.scope.locale.MockRefundSimulatorHelp()}
368
+ </p>
369
+ <button
370
+ class="btn btn-info btn-sm mt-2"
371
+ type="button"
372
+ disabled={this._mockCompletion?.isPending}
373
+ onClick={() => this._completeMockRefund()}
374
+ >
375
+ {this._mockCompletion?.isPending && (
376
+ <span class="loading loading-spinner loading-xs"></span>
377
+ )}
378
+ {this.scope.locale.CompleteMockRefund()}
379
+ </button>
380
+ {this.mockCompletionError && (
381
+ <p role="alert" class="mt-2 text-error">
382
+ {this.scope.locale.MockRefundCompletionFailed()}
383
+ </p>
384
+ )}
385
+ </>
386
+ )}
387
+ </div>
388
+ )}
389
+ {recoveryData.recoveryDisposition !== 'none' &&
390
+ recoveryData.recoveryDisposition !== 'await_webhook' && (
391
+ <input
392
+ class="input input-bordered input-sm mt-2 w-full"
393
+ name="recoveryReason"
394
+ placeholder={this.scope.locale.RefundRecoveryReason()}
395
+ v-model={this.recoveryReason}
396
+ />
397
+ )}
398
+ {(recoveryData.recoveryDisposition === 'query_only' ||
399
+ recoveryData.recoveryDisposition === 'reconcile_only') && (
400
+ <button
401
+ class="btn btn-outline btn-sm mt-2"
402
+ type="button"
403
+ disabled={this._recoveryActionPending}
404
+ onClick={() => this._reconcile()}
405
+ >
406
+ {this.scope.locale.ReconcileProviderRefund()}
407
+ </button>
408
+ )}
409
+ {recoveryData.recoveryDisposition === 'retry_same_key' && (
410
+ <>
411
+ <label class="label cursor-pointer mt-2 justify-start gap-2">
412
+ <input
413
+ class="checkbox checkbox-sm"
414
+ name="retryRiskAcknowledged"
415
+ type="checkbox"
416
+ v-model={this.retryRiskAcknowledged}
417
+ />
418
+ <span class="label-text text-xs">
419
+ {this.scope.locale.AcknowledgeRefundRetryRisk()}
420
+ </span>
421
+ </label>
422
+ <button
423
+ class="btn btn-warning btn-sm"
424
+ type="button"
425
+ disabled={this._recoveryActionPending}
426
+ onClick={() => this._retry()}
427
+ >
428
+ {this.scope.locale.RetryRefund()}
429
+ </button>
430
+ </>
431
+ )}
432
+ </>
433
+ )}
434
+ </section>
435
+ )}
436
+ </div>
437
+ );
438
+ },
439
+ slotActions: dialog => {
440
+ const disabled = !this.executionConfirmed || this._executionPending;
441
+ return (
442
+ <>
443
+ {this._executionPending && <span class="loading loading-spinner text-primary"></span>}
444
+ <button class="btn btn-ghost" type="button" onClick={dialog.close}>
445
+ {this.scope.locale.Cancel()}
446
+ </button>
187
447
  <button
188
- class="btn btn-outline btn-sm mt-2"
448
+ class="btn btn-warning"
189
449
  type="button"
190
- onClick={() => this._reconcile()}
450
+ disabled={disabled}
451
+ onClick={() => this._execute(dialog.close)}
191
452
  >
192
- {this.scope.locale.ReconcileProviderRefund()}
453
+ {this.scope.locale.ExecuteRefund()}
193
454
  </button>
194
- )}
195
- {recoveryData.recoveryDisposition === 'retry_same_key' && (
196
- <>
197
- <label class="label cursor-pointer mt-2 justify-start gap-2">
198
- <input
199
- class="checkbox checkbox-sm"
200
- name="retryRiskAcknowledged"
201
- type="checkbox"
202
- v-model={this.retryRiskAcknowledged}
203
- />
204
- <span class="label-text text-xs">
205
- {this.scope.locale.AcknowledgeRefundRetryRisk()}
206
- </span>
207
- </label>
208
- <button class="btn btn-warning btn-sm" type="button" onClick={() => this._retry()}>
209
- {this.scope.locale.RetryRefund()}
210
- </button>
211
- </>
212
- )}
213
- </section>
214
- )}
215
- </div>
455
+ </>
456
+ );
457
+ },
458
+ onClose: () => this._resetExecutionDialogState(),
459
+ },
460
+ {
461
+ closeOnBackdrop: false,
462
+ closeOnEscape: false,
463
+ maxWidth: 560,
464
+ showCloseButton: true,
465
+ },
466
+ );
467
+ }
468
+
469
+ protected render() {
470
+ const props = this.$props as ControllerTableCellActionRefundProps;
471
+ const order = this.$$renderContext.cellContext.row
472
+ .original as ApiSchemaCommerceTradeDtoOrderSelectResItem;
473
+ if (order.state === 'refund_requested') {
474
+ return (
475
+ <span
476
+ class={props.class}
477
+ role="button"
478
+ tabindex={0}
479
+ onClick={() => this._openRefundDecisionDialog()}
480
+ onKeydown={event => {
481
+ if (event.key === 'Enter' || event.key === ' ') {
482
+ event.preventDefault();
483
+ this._openRefundDecisionDialog();
484
+ }
485
+ }}
486
+ >
487
+ {this.scope.locale.ReviewRefund()}
488
+ </span>
489
+ );
490
+ }
491
+ return (
492
+ <button class={props.class} type="button" onClick={() => this._openRefundExecutionDialog()}>
493
+ {this.scope.locale.ExecuteRefund()}
494
+ </button>
216
495
  );
217
496
  }
218
497
  }