cabloy 5.1.124 → 5.1.126
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/.cabloy-version +1 -1
- package/.claude/skills/cabloy-backend-scaffold/SKILL.md +1 -1
- package/.claude/skills/cabloy-backend-scaffold/evals/evals.json +6 -0
- package/.claude/skills/cabloy-backend-scaffold/references/follow-up-checklist.md +13 -4
- package/.claude/skills/cabloy-workflow/SKILL.md +4 -2
- package/.claude/skills/cabloy-workflow/evals/evals.json +14 -2
- package/.claude/skills/cabloy-worktree-environment/SKILL.md +206 -0
- package/.claude/skills/cabloy-worktree-environment/evals/evals.json +77 -0
- package/.github/workflows/vona-test-pg.yml +3 -0
- package/.github/workflows/vona-test-sqlite3.yml +3 -0
- package/CHANGELOG.md +21 -0
- package/CLAUDE.md +1 -1
- package/cabloy-docs/.vitepress/config.mjs +2 -0
- package/cabloy-docs/ai/playbook-technical-blog-authoring.md +158 -0
- package/cabloy-docs/backend/backend-contract-emission-specimen.md +2 -2
- package/cabloy-docs/backend/dto-guide.md +10 -7
- package/cabloy-docs/backend/dto-infer-generation.md +127 -7
- package/cabloy-docs/backend/foundation.md +1 -1
- package/cabloy-docs/backend/introduction.md +1 -0
- package/cabloy-docs/backend/module-dependencies.md +104 -0
- package/cabloy-docs/backend/serialization-guide.md +6 -0
- package/cabloy-docs/fullstack/parallel-worktree-environment.md +72 -34
- package/cabloy-docs/fullstack/suites-and-modules.md +1 -0
- package/cabloy-docs/reference/package-map.md +1 -0
- package/e2e/specs/a-commerce/commerce.spec.ts +89 -0
- package/package.json +2 -1
- package/scripts/init.ts +7 -12
- package/scripts/initAppName.test.ts +72 -0
- package/scripts/initAppName.ts +50 -0
- package/vona/packages-vona/vona/package.json +1 -1
- package/vona/pnpm-lock.yaml +48 -146
- package/vona/src/suite/a-commerce/modules/commerce-member/src/dto/addressMineCreate.tsx +6 -27
- package/vona/src/suite/a-commerce/modules/commerce-member/src/dto/addressMineItem.tsx +6 -31
- package/vona/src/suite/a-commerce/modules/commerce-member/src/dto/addressMineUpdate.tsx +6 -2
- package/vona/src/suite/a-commerce/modules/commerce-member/src/lib/addressMine.ts +16 -0
- package/vona/src/suite/a-commerce/modules/commerce-member/src/service/address.ts +4 -17
- package/vona/src/suite/a-commerce/modules/commerce-member/test/addressOwnership.test.ts +63 -19
- package/vona/src/suite/a-commerce/modules/commerce-payment/package.json +2 -1
- package/vona/src/suite/a-commerce/modules/commerce-payment/src/bean/payScene.commerceOrder.ts +11 -2
- package/vona/src/suite/a-commerce/modules/commerce-payment/src/service/commercePayScene.ts +26 -0
- package/vona/src/suite/a-commerce/modules/commerce-payment/test/paymentAttempt.test.ts +19 -2
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/config/locale/en-us.ts +5 -2
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/config/locale/zh-cn.ts +5 -2
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponTemplateCreate.tsx +26 -35
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/dto/couponTemplateUpdate.tsx +6 -1
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/entity/couponTemplate.tsx +40 -4
- package/vona/src/suite/a-commerce/modules/commerce-promotion/test/couponTemplateFormLayout.test.ts +85 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/.metadata/index.ts +1 -1
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/controller/order.ts +3 -5
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/orderSummary.tsx +7 -40
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/shipmentView.tsx +6 -15
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/lib/order.ts +9 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/order.ts +121 -11
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/order.test.ts +25 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/paymentOutcome.test.ts +19 -2
- package/vona/src/suite/a-training/modules/training-record/src/dto/detailRecordSubjectResItem.tsx +6 -1
- package/vona/src/suite/a-training/modules/training-record/src/dto/recordSelectResItem.tsx +3 -1
- package/vona/src/suite/a-training/modules/training-record/src/dto/recordView.tsx +4 -1
- package/vona/src/suite/a-training/modules/training-record/test/record.test.ts +45 -0
- package/vona/src/suite/a-training/modules/training-student/src/dto/detailRecordResItem.tsx +6 -1
- package/vona/src/suite/a-training/modules/training-student/src/dto/studentSummary.tsx +5 -23
- package/vona/src/suite/a-training/modules/training-student/test/student.test.ts +37 -5
- package/vona/src/suite-vendor/a-pay/modules/a-pay/package.json +1 -1
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/.metadata/index.ts +29 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/meta.index.ts +3 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/meta.redlock.ts +3 -1
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/meta.version.ts +7 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/queue.outboxDispatch.ts +9 -4
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/schedule.providerOperationDispatch.ts +11 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/entity/outboxEvent.tsx +11 -4
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/entity/providerOperation.tsx +12 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/entity/webhookInbox.tsx +6 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/outbox.ts +12 -5
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/paymentSession.ts +1 -37
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/providerOperation.ts +355 -1
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/refundOperation.ts +226 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/webhook.ts +148 -50
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/types/payScene.ts +2 -1
- package/vona/src/suite-vendor/a-pay/modules/pay-mock/package.json +1 -1
- package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/.metadata/index.ts +39 -22
- package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/bean/payProvider.mock.ts +42 -13
- package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/controller/mockPayment.ts +11 -0
- package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/dto/mockRefundComplete.tsx +13 -0
- package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/dto/mockRefundReceipt.tsx +16 -0
- package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/service/payMock.ts +56 -0
- package/vona/src/suite-vendor/a-pay/modules/pay-paypal/package.json +2 -1
- package/vona/src/suite-vendor/a-pay/modules/pay-paypal/src/bean/payProvider.paypal.ts +117 -8
- package/vona/src/suite-vendor/a-pay/modules/pay-stripe/package.json +1 -1
- package/vona/src/suite-vendor/a-pay/package.json +5 -5
- package/vona/src/suite-vendor/a-vona/modules/a-orm/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/dto/dtoGet.ts +5 -5
- package/vona/src/suite-vendor/a-vona/package.json +1 -1
- package/zova/src/suite/a-commerce/modules/commerce-member/src/api/openapi/schemas.ts +54 -12
- package/zova/src/suite/a-commerce/modules/commerce-member/src/api/openapi/types.ts +625 -64
- package/zova/src/suite/a-commerce/modules/commerce-promotion/src/api/openapi/types.ts +55 -15
- package/zova/src/suite/a-commerce/modules/commerce-trade/cli/openapi.config.ts +1 -1
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/commerceTradeOrder.ts +15 -17
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/schemas.ts +12 -8
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/types.ts +111 -26
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/apiSchema/commerceTradeOrder.ts +3 -3
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/component/tableCellActionRefund/controller.tsx +1 -8
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/model/order.ts +5 -9
- package/zova/src/suite/a-training/modules/training-student/src/.metadata/index.ts +1 -1
- package/zova/src/suite/a-training/modules/training-student/src/api/openapi/schemas.ts +301 -17
- package/zova/src/suite/a-training/modules/training-student/src/api/openapi/types.ts +5461 -1788
- package/vona/src/suite-vendor/a-pay/modules/a-pay/test/outbox.test.ts +0 -209
- package/vona/src/suite-vendor/a-pay/modules/a-pay/test/paymentSession.test.ts +0 -153
- package/vona/src/suite-vendor/a-pay/modules/a-pay/test/webhook.test.ts +0 -278
- package/vona/src/suite-vendor/a-pay/modules/pay-mock/test/payMock.test.ts +0 -141
|
@@ -101,8 +101,8 @@ The next source to read is:
|
|
|
101
101
|
Representative source facts:
|
|
102
102
|
|
|
103
103
|
- `@Dto<IDtoOptionsStudentSummary>()`
|
|
104
|
-
-
|
|
105
|
-
- `@Api.field(...)` metadata
|
|
104
|
+
- the `ModelStudent` projection supplies `id`, `name`, `mobile`, `level`, and `description`
|
|
105
|
+
- direct `@Api.field(...)` metadata declares the computed `levelTitle`, `descriptionLength`, and `summaryText` fields
|
|
106
106
|
|
|
107
107
|
This file answers the second emission question:
|
|
108
108
|
|
|
@@ -148,7 +148,9 @@ A practical split is:
|
|
|
148
148
|
- use inferred DTOs when the contract closely follows model structure or query shape
|
|
149
149
|
- wrap inferred DTOs in a named DTO class when reuse or discoverability becomes more important
|
|
150
150
|
|
|
151
|
-
|
|
151
|
+
When stable Entity, Model, relation, or query truth already exists, prefer inference first: use `$Dto.*` with `columns`, `include`, or `dtoClass` to define the projection; use `@Dto({ fields })` with `$makeMetadata(...)` for metadata-only differences or `$makeSchema(...)` for schema and validation differences; add an `@Api.field(...)` member only for a genuinely new field absent from the inferred contract. Keep an explicit DTO as a deliberate fallback when no suitable upstream truth exists or the inferred composition is less clear.
|
|
152
|
+
|
|
153
|
+
Advanced inferred DTO shaping can also stay named and reusable through helper options such as `dtoClass`, especially for relation-aware contracts and nested DTO surfaces. `$Dto.get(...)` otherwise keeps the complete model-aware read shape by default; use `columns` or `dtoClass` for a genuine business projection, not merely to remove `iid` or `deleted`. For the canonical authoring sequence, see [Default-first three-layer DTO authoring](/backend/dto-infer-generation#default-first-three-layer-dto-authoring) and [Default read shape versus a public projection](/backend/dto-infer-generation#default-read-shape-versus-a-public-projection).
|
|
152
154
|
|
|
153
155
|
## Relationship to ORM and controller contracts
|
|
154
156
|
|
|
@@ -166,12 +168,13 @@ That means DTO design should often be read together with:
|
|
|
166
168
|
|
|
167
169
|
When creating DTOs:
|
|
168
170
|
|
|
169
|
-
1.
|
|
170
|
-
2.
|
|
171
|
-
3.
|
|
172
|
-
4.
|
|
173
|
-
5.
|
|
174
|
-
6.
|
|
171
|
+
1. check whether an Entity, Model, relation, or query shape already provides suitable upstream contract truth
|
|
172
|
+
2. define the inferred projection before redeclaring fields: choose the `$Dto.*` helper and, where needed, `columns`, `include`, or `dtoClass`
|
|
173
|
+
3. use `$makeMetadata(...)` for metadata-only refinement and `$makeSchema(...)` for schema or validation refinement of an inferred field
|
|
174
|
+
4. use `@Api.field(...)` for a genuinely new field, not as a second declaration of an inferred field
|
|
175
|
+
5. keep DTO validation and OpenAPI concerns aligned, and keep the final structure-defining schema last when using schema-like composition
|
|
176
|
+
6. decide whether the contract should remain inferred, be wrapped in a named inferred DTO, or deliberately fall back to an explicit DTO
|
|
177
|
+
7. treat DTO design as part of the contract between backend handlers, models, and frontend integration
|
|
175
178
|
|
|
176
179
|
## Where to read next
|
|
177
180
|
|
|
@@ -94,6 +94,16 @@ A practical mental model is:
|
|
|
94
94
|
|
|
95
95
|
This matters because different ORM operations naturally produce different API contracts.
|
|
96
96
|
|
|
97
|
+
## Default read shape versus a public projection
|
|
98
|
+
|
|
99
|
+
`$Dto.get(() => ModelX)` defaults to the complete model-aware Entity read shape, including inherited framework fields such as `iid` and `deleted` when the Entity defines them. This is the ordinary read baseline, not an automatic public-payload minimization policy.
|
|
100
|
+
|
|
101
|
+
Use `columns` when an endpoint genuinely needs a smaller one-off business, query, or response projection. Use `dtoClass` when that curated read surface is named and reusable, especially for nested relation contracts. Do not add either solely to remove `iid` or `deleted` from an otherwise complete `$Dto.get(...)` shape.
|
|
102
|
+
|
|
103
|
+
This differs from top-level `$Dto.create(...)` and `$Dto.update(...)`. Their default omission of identity, active-instance, soft-deletion, and lifecycle fields establishes a write-input authorization boundary: callers must not supply those framework-owned values. That write policy does not imply that ordinary read DTOs need the same narrowing.
|
|
104
|
+
|
|
105
|
+
A narrowed DTO or emitted OpenAPI schema declares the supported contract, but does not by itself prove that an already-built HTTP response object is projected or stripped at runtime. When actual payload minimization is required, deliberately shape the returned data or apply a verified response-output policy, then verify the action response. See [Serialization Guide](/backend/serialization-guide) for response transformation behavior.
|
|
106
|
+
|
|
97
107
|
## When inference should replace handwritten DTOs
|
|
98
108
|
|
|
99
109
|
A practical rule is:
|
|
@@ -106,12 +116,117 @@ A practical reading takeaway is:
|
|
|
106
116
|
|
|
107
117
|
> the best default is often not “handwritten DTO or no DTO.” It is “named DTO class backed by the right inferred helper.”
|
|
108
118
|
|
|
119
|
+
## Default-first three-layer DTO authoring
|
|
120
|
+
|
|
121
|
+
The three DTO shapes above classify the public artifact: explicit named DTO, inline inference, or a named class that wraps inference. They do not describe how to author an inferred contract.
|
|
122
|
+
|
|
123
|
+
When an Entity, Model, relation, or query shape already owns stable contract truth, start from that truth instead of redeclaring its fields. Define the DTO projection first, then add only the smallest DTO-local difference. This preserves inherited validation, titles, OpenAPI metadata, and render metadata wherever they still apply.
|
|
124
|
+
|
|
125
|
+
### Layer 1: project upstream truth
|
|
126
|
+
|
|
127
|
+
Use the operation-appropriate `$Dto.*` helper to establish the baseline contract. Then choose the narrowest projection mechanism:
|
|
128
|
+
|
|
129
|
+
- use the default inferred shape when all relevant model-aware fields belong in the DTO
|
|
130
|
+
- use `columns` for a simple local field subset, including the exclusion of server-owned fields
|
|
131
|
+
- use `include` to bring a relation-aware shape into the DTO
|
|
132
|
+
- use `dtoClass` when a top-level or nested relation needs a reusable, named field surface
|
|
133
|
+
|
|
134
|
+
For example, `DtoStudentSelectReq` starts from an Entity-backed query projection:
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
export class DtoStudentSelectReq extends $Dto.queryPage(EntityStudent, [
|
|
138
|
+
'name',
|
|
139
|
+
'level',
|
|
140
|
+
'createdAt',
|
|
141
|
+
]) {}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
A relation-aware DTO can use `columns` and `include` to curate its baseline without restating each field decorator:
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
export class DtoDetailRecordBase extends $Dto.get(() => ModelRecord, {
|
|
148
|
+
columns: ['id', 'name', 'subjectCount', 'totalScore', 'averageScore'],
|
|
149
|
+
include: { trainingRecordSubjects: true },
|
|
150
|
+
}) {}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Use `dtoClass` when that curated surface is itself reusable. For example, a detail mutate DTO can take its field surface from `DtoDetailRecordBase`, and a parent create DTO can include that named detail contract:
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
export class DtoDetailRecordMutate extends $Dto.mutate(() => ModelRecord, {
|
|
157
|
+
dtoClass: DtoDetailRecordBase,
|
|
158
|
+
include: { trainingRecordSubjects: { dtoClass: DtoDetailRecordSubjectMutate } },
|
|
159
|
+
}) {}
|
|
160
|
+
|
|
161
|
+
export class DtoStudentCreate extends $Dto.create(() => ModelStudent, {
|
|
162
|
+
include: { trainingRecords: { dtoClass: DtoDetailRecordMutate } },
|
|
163
|
+
}) {}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
When a field genuinely does not belong to an API's business read or write contract, exclude it through the projection; do not rely on visibility metadata to hide a field that the API must not accept or return. Do not add a projection solely to remove framework read fields such as `iid` or `deleted` from an otherwise complete `$Dto.get(...)` shape; see [Default read shape versus a public projection](#default-read-shape-versus-a-public-projection).
|
|
167
|
+
|
|
168
|
+
### Layer 2: overlay metadata or refine the schema
|
|
169
|
+
|
|
170
|
+
For a field already supplied by the projection, use the `fields` map on `@Dto(...)` rather than redeclaring the property:
|
|
171
|
+
|
|
172
|
+
- use `$makeMetadata(...)` when only field metadata changes, such as title, order, visibility, serialization, or render behavior; the inherited schema and validation remain authoritative
|
|
173
|
+
- use `$makeSchema(...)` when optionality, validation, enum members, preprocess/transform behavior, or another schema structure must change; it replaces or refines the runtime field schema while preserving the framework's inherited OpenAPI metadata merge
|
|
174
|
+
|
|
175
|
+
`DtoStudentSelectReq` demonstrates schema refinement for query input:
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
@Dto({
|
|
179
|
+
fields: {
|
|
180
|
+
name: $makeSchema(v.optional(), z.string()),
|
|
181
|
+
level: $makeSchema(v.optional(), z.number()),
|
|
182
|
+
createdAt: $makeSchema(v.filterTransform('a-web:dateRange'), v.optional(), z.string()),
|
|
183
|
+
},
|
|
184
|
+
})
|
|
185
|
+
export class DtoStudentSelectReq extends $Dto.queryPage(EntityStudent, [
|
|
186
|
+
'name',
|
|
187
|
+
'level',
|
|
188
|
+
'createdAt',
|
|
189
|
+
]) {}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
`$makeSchema(...)` applies schema-like arguments right-to-left. Keep the final structure-defining schema, such as `z.string()`, `z.number()`, `v.object(...)`, or `v.array(...)`, last in authoring order. Treat optionality, nullability, defaults, preprocess/transform wrappers, objects, and arrays as structure-shaping rather than metadata-only, and verify emitted schema/OpenAPI output after changing them.
|
|
193
|
+
|
|
194
|
+
`@Dto({ fields })` changes the runtime contract and metadata. It does not rewrite the TypeScript property type inferred from the `$Dto.*` base class. Do not add a duplicate `declare` field or a second field decorator solely to mirror a runtime schema restriction unless a separate static contract is genuinely required and is type-compatible with the inferred base.
|
|
195
|
+
|
|
196
|
+
### Layer 3: add contract-only fields
|
|
197
|
+
|
|
198
|
+
Declare a class member with `@Api.field(...)` only when the field is absent from the inferred projection. Typical examples are serializer-produced response fields, operation-local helper fields, or a separate nested representation.
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
export class DtoStudentCreate extends $Dto.create(() => ModelStudent, {
|
|
202
|
+
include: { trainingRecords: { dtoClass: DtoDetailRecordMutate } },
|
|
203
|
+
}) {
|
|
204
|
+
@Api.field(v.optional(), v.array(DtoDetailRecordResItem))
|
|
205
|
+
_trainingRecords?: DtoDetailRecordResItem[];
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Do not manually redeclare an inferred field merely to adjust its title, renderer, validation, or schema. Use Layer 2 instead.
|
|
210
|
+
|
|
211
|
+
### When to use an explicit DTO from scratch
|
|
212
|
+
|
|
213
|
+
Inference first is a default strategy, not a requirement to force every DTO through `$Dto.*`. Use an explicit DTO deliberately when no stable upstream Entity, Model, relation, or query shape is suitable, for example:
|
|
214
|
+
|
|
215
|
+
- authentication, captcha, or behavior-focused commands
|
|
216
|
+
- webhook and third-party protocol payloads
|
|
217
|
+
- aggregate commands assembled from unrelated resources
|
|
218
|
+
- a public contract that must stay intentionally decoupled from persistence structure
|
|
219
|
+
|
|
220
|
+
If the inferred baseline would require pervasive exceptions or no longer makes the contract clearer, an explicit DTO is the better design.
|
|
221
|
+
|
|
109
222
|
## `training-student` as the decision specimen
|
|
110
223
|
|
|
111
224
|
The current `training-student` module is a strong specimen because it shows several different DTO choices in one compact family.
|
|
112
225
|
|
|
113
226
|
Relevant source files include:
|
|
114
227
|
|
|
228
|
+
- `vona/src/suite/a-training/modules/training-student/src/dto/detailRecordBase.tsx`
|
|
229
|
+
- `vona/src/suite/a-training/modules/training-student/src/dto/detailRecordMutate.tsx`
|
|
115
230
|
- `vona/src/suite/a-training/modules/training-student/src/dto/studentCreate.tsx`
|
|
116
231
|
- `vona/src/suite/a-training/modules/training-student/src/dto/studentUpdate.tsx`
|
|
117
232
|
- `vona/src/suite/a-training/modules/training-student/src/dto/studentView.tsx`
|
|
@@ -163,10 +278,12 @@ Representative source facts:
|
|
|
163
278
|
|
|
164
279
|
This makes it a strong specimen of a **query DTO that still wraps inference, but adds operation-specific shaping**.
|
|
165
280
|
|
|
281
|
+
Its `fields` entries use `$makeSchema(...)` because the query contract changes the projected fields' runtime schema: query values become optional, `level` is normalized before validation, and `createdAt` accepts the date-range filter representation. Keep the final structure-defining schema last in each `$makeSchema(...)` call, then verify the emitted schema/OpenAPI result after a structure-shaping change.
|
|
282
|
+
|
|
166
283
|
A practical reading takeaway is:
|
|
167
284
|
|
|
168
285
|
- inference gives the structural baseline
|
|
169
|
-
-
|
|
286
|
+
- `$makeSchema(...)` refines projected fields when their query contract behavior differs
|
|
170
287
|
|
|
171
288
|
### Row-item response DTO
|
|
172
289
|
|
|
@@ -241,7 +358,7 @@ A practical rule is:
|
|
|
241
358
|
|
|
242
359
|
## Use `dtoClass` to shape inferred fields
|
|
243
360
|
|
|
244
|
-
When inferred DTOs should still follow a reusable named field surface, pass `dtoClass` to the helper options.
|
|
361
|
+
When inferred DTOs should still follow a reusable named field surface, pass `dtoClass` to the helper options. This is the reusable projection choice from [Layer 1: project upstream truth](#layer-1-project-upstream-truth).
|
|
245
362
|
|
|
246
363
|
This is useful when:
|
|
247
364
|
|
|
@@ -357,11 +474,14 @@ For the bridge step that carries this backend-authored contract across the stack
|
|
|
357
474
|
|
|
358
475
|
When evaluating a return shape or input contract that closely follows model structure, ask:
|
|
359
476
|
|
|
360
|
-
1.
|
|
361
|
-
2.
|
|
362
|
-
3.
|
|
477
|
+
1. does an Entity, Model, relation, or query shape already provide stable upstream contract truth?
|
|
478
|
+
2. is the contract get/list/query/create/update/aggregate/group oriented, and which `$Dto.*` helper matches it?
|
|
479
|
+
3. should `columns`, `include`, `with`, or `dtoClass` define the projection boundary?
|
|
363
480
|
4. should the inferred DTO stay inline or be wrapped in a named DTO class?
|
|
364
|
-
5.
|
|
365
|
-
6. is
|
|
481
|
+
5. for every local difference, is it metadata-only (`$makeMetadata(...)`) or schema-affecting (`$makeSchema(...)`)?
|
|
482
|
+
6. is every `@Api.field(...)` member genuinely new instead of a redeclared inferred field?
|
|
483
|
+
7. if `$makeSchema(...)` is used, is the structure-defining schema last and is emitted schema/OpenAPI verification planned?
|
|
484
|
+
8. does the resulting DTO also affect OpenAPI and frontend generation paths?
|
|
485
|
+
9. is CRUD generation already giving enough contract structure that another handwritten DTO would be redundant, or is an explicit DTO clearer?
|
|
366
486
|
|
|
367
487
|
That helps reduce redundant type work and keeps contracts closer to the model truth.
|
|
@@ -263,7 +263,7 @@ Using `this.$scope.<fixedModule>`, `app.scope(...)`, or `this.app.scope(...)` do
|
|
|
263
263
|
|
|
264
264
|
Scope lookup also does not compose, install, load, or order an absent module. The target must already be available through suite/application composition. Declare `vonaModule.dependencies` only when the caller has a genuine requirement for a target module's availability, dependency-first ordering, or minimum compatible version—not merely because it looks up that module's service, model, config, locale, or another resource.
|
|
265
265
|
|
|
266
|
-
For the package, suite, and module
|
|
266
|
+
For the canonical availability, ordering, and version decision guide, see [Vona Module Dependencies](/backend/module-dependencies). For the package, suite, and module topology distinction, see [Package Map](/reference/package-map).
|
|
267
267
|
|
|
268
268
|
## Suite / module / package boundaries
|
|
269
269
|
|
|
@@ -30,6 +30,7 @@ Use this page as the main backend hub, then choose the family that matches your
|
|
|
30
30
|
Start here when you need the core backend mental model first:
|
|
31
31
|
|
|
32
32
|
- [Backend Foundation](/backend/foundation)
|
|
33
|
+
- [Vona Module Dependencies](/backend/module-dependencies)
|
|
33
34
|
- [Backend Essentials](/backend/backend-essentials)
|
|
34
35
|
- [Backend CLI](/backend/cli)
|
|
35
36
|
- [Service Guide](/backend/service-guide)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Vona Module Dependencies
|
|
2
|
+
|
|
3
|
+
Use `vonaModule.dependencies` when one Vona module has a real requirement for another module's availability, dependency-first ordering, or minimum compatible version.
|
|
4
|
+
|
|
5
|
+
It is not a general declaration for every cross-module reference.
|
|
6
|
+
|
|
7
|
+
## What a module dependency means
|
|
8
|
+
|
|
9
|
+
A dependency in a module package manifest expresses all of these conditions:
|
|
10
|
+
|
|
11
|
+
- the target module must be present and enabled
|
|
12
|
+
- the target module is ordered before the dependent module
|
|
13
|
+
- the declared version is the minimum compatible target version
|
|
14
|
+
|
|
15
|
+
For example:
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
{
|
|
19
|
+
"vonaModule": {
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"a-vona": "5.0.0",
|
|
22
|
+
"a-telemetry": "5.0.0"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
This is a module lifecycle contract. It is stronger than an import or a resource lookup.
|
|
29
|
+
|
|
30
|
+
## Four different relationships
|
|
31
|
+
|
|
32
|
+
Keep these concerns separate.
|
|
33
|
+
|
|
34
|
+
| Relationship | Purpose | Typical surface |
|
|
35
|
+
| -------------------------------- | ---------------------------------------------------------------------- | ---------------------------------- |
|
|
36
|
+
| Package dependency | Makes a package available to package tooling | `package.json` `dependencies` |
|
|
37
|
+
| Suite or application composition | Includes module packages in the application composition | suite/application package metadata |
|
|
38
|
+
| Module dependency | Requires a target module's availability, order, and compatible version | `vonaModule.dependencies` |
|
|
39
|
+
| Runtime resource lookup | Resolves a resource from an already composed module | `this.$scope`, `app.scope(...)` |
|
|
40
|
+
|
|
41
|
+
A module dependency does not install a package or compose an otherwise absent module. Ensure package and suite/application composition separately.
|
|
42
|
+
|
|
43
|
+
## Lookup is not a dependency edge
|
|
44
|
+
|
|
45
|
+
The following forms look up resources from modules that are already composed into the active application:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
this.scope.model.order;
|
|
49
|
+
this.$scope.commerceCatalog.model.product;
|
|
50
|
+
app.scope('commerce-catalog').model.product;
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Lookup alone does not require adding the target to `vonaModule.dependencies`. That includes lookup of another module's service, model, config, locale, entity, or other scoped resource.
|
|
54
|
+
|
|
55
|
+
The same distinction applies to a named ORM relation or a relation included by an inferred DTO. A relation such as:
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
$relation.belongsTo('commerce-trade:order', 'commerce-member:user', 'userId');
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
is not, by itself, a reason to add a module dependency edge.
|
|
62
|
+
|
|
63
|
+
## Decision guide
|
|
64
|
+
|
|
65
|
+
| Question | Decision |
|
|
66
|
+
| -------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
|
|
67
|
+
| Does the code only look up a resource from an already composed module? | Do not add an edge solely for the lookup. |
|
|
68
|
+
| Can the feature operate when the target module is absent? | Do not add a required dependency; make optional behavior explicit. |
|
|
69
|
+
| Must the target module be present and enabled for the feature to work? | Add a module dependency and verify composition separately. |
|
|
70
|
+
| Must the target initialize before the dependent module? | Add a module dependency. |
|
|
71
|
+
| Does startup, lifecycle, or `monkey.ts` integration depend on deterministic target ordering? | Add a module dependency. |
|
|
72
|
+
| Does the feature require a minimum compatible target-module version? | Add a module dependency with that minimum version. |
|
|
73
|
+
| Would the proposed edge create a cycle? | Revisit module ownership or composition instead of encoding a circular dependency. |
|
|
74
|
+
|
|
75
|
+
`monkey.ts` and lifecycle integration are common reasons to require ordering, but they are not the only valid reason. The decisive question is whether the target module is a required availability, ordering, or version contract.
|
|
76
|
+
|
|
77
|
+
## Avoid speculative edges
|
|
78
|
+
|
|
79
|
+
Do not add `vonaModule.dependencies` merely to:
|
|
80
|
+
|
|
81
|
+
- document an import
|
|
82
|
+
- document a scope lookup
|
|
83
|
+
- make a named ORM relation appear explicit
|
|
84
|
+
- compensate for an assumed circular lookup
|
|
85
|
+
- encode a transient implementation detail that should remain inside the framework
|
|
86
|
+
|
|
87
|
+
A speculative edge can make a module unavailable when its target is disabled, impose an unnecessary ordering constraint, and turn a manageable design issue into a cycle.
|
|
88
|
+
|
|
89
|
+
## Verification
|
|
90
|
+
|
|
91
|
+
When adding or changing a module dependency:
|
|
92
|
+
|
|
93
|
+
1. verify that package and suite/application composition makes the target available
|
|
94
|
+
2. verify the target module's relative name and minimum compatible version
|
|
95
|
+
3. confirm that the dependency graph remains acyclic
|
|
96
|
+
4. run the relevant Vona metadata, build, or startup path that consumes module metadata
|
|
97
|
+
5. run the narrowest meaningful tests and type checks for the affected feature
|
|
98
|
+
|
|
99
|
+
## Related guides
|
|
100
|
+
|
|
101
|
+
- [Backend Foundation](/backend/foundation#scope-lookup-vs-module-dependencies)
|
|
102
|
+
- [Package Map](/reference/package-map)
|
|
103
|
+
- [Backend Startup Guide](/backend/startup-guide)
|
|
104
|
+
- [Backend CLI](/backend/cli)
|
|
@@ -51,6 +51,12 @@ A practical debugging rule is:
|
|
|
51
51
|
|
|
52
52
|
- if a field-level serializer appears correct but the response still returns the raw value, first check whether the controller action uses `@Core.serializer()`
|
|
53
53
|
|
|
54
|
+
### Declared response shape versus physical output fields
|
|
55
|
+
|
|
56
|
+
A DTO or `@Api.body(...)` schema declares the response contract and emitted OpenAPI shape. It is not, by itself, an unknown-field stripping pass over an object that a controller action has already returned.
|
|
57
|
+
|
|
58
|
+
When an API must physically omit fields from its payload, deliberately return the intended projection or apply a verified response-output policy, then verify the action response. Field-level serializer metadata is part of such output behavior only when the action enables `@Core.serializer()`; do not assume a narrower DTO declaration alone sanitizes a response. For deciding whether a read DTO itself needs a narrower business projection, see [Default read shape versus a public projection](/backend/dto-infer-generation#default-read-shape-versus-a-public-projection).
|
|
59
|
+
|
|
54
60
|
## Serializer transforms
|
|
55
61
|
|
|
56
62
|
Vona supports custom serializer transforms through `@SerializerTransform(...)`.
|
|
@@ -1,81 +1,119 @@
|
|
|
1
1
|
# Parallel Worktree Environment
|
|
2
2
|
|
|
3
|
-
Use worktree-local environment overrides when two Cabloy Basic worktrees need to run ordinary local development or managed E2E checks at the same time.
|
|
3
|
+
Use worktree-local environment overrides when two Cabloy Basic or Cabloy Start worktrees need to run ordinary local development or managed E2E checks at the same time.
|
|
4
4
|
|
|
5
|
-
This
|
|
5
|
+
This Cabloy Basic page is the canonical shared recipe for both editions. Detect the active edition before selecting commands, but keep the same two-file environment model.
|
|
6
|
+
|
|
7
|
+
This workflow creates a standard isolated local runtime without changing committed environment defaults. It does not automatically isolate every external dependency.
|
|
6
8
|
|
|
7
9
|
## Before creating local overrides
|
|
8
10
|
|
|
9
|
-
1.
|
|
10
|
-
2.
|
|
11
|
-
3. Choose one unique worktree name and unused Vona, Zova, and HMR ports.
|
|
11
|
+
1. Create and enter a separate linked worktree.
|
|
12
|
+
2. Explicitly invoke `/cabloy-worktree-environment` to receive a deterministic environment proposal and confirm it before any local file is written.
|
|
12
13
|
|
|
13
|
-
The repository ignores `**/env/.env*.local`,
|
|
14
|
+
The repository ignores `**/env/.env*.local`, but this workflow may create or change only these broad files:
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
- `vona/env/.env.local`
|
|
17
|
+
- `zova/env/.env.local`
|
|
16
18
|
|
|
17
|
-
|
|
19
|
+
Do not create or modify flavor-, mode-, app-mode-, or runtime-specific `.env.*.local` files for worktree isolation.
|
|
18
20
|
|
|
19
|
-
|
|
21
|
+
## Deterministic, secret-safe recommendations
|
|
20
22
|
|
|
21
|
-
|
|
23
|
+
The explicit setup skill does not inspect `.env`, `.env.local`, `.env.*.local`, sibling configuration, process environment, listening processes, or external services while recommending values. Therefore secrets in local environment files are not supplied to the AI/model, printed to the console, or included in this workflow’s diagnostic output.
|
|
22
24
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
The recommendation uses only Git worktree metadata and the fixed defaults below. The primary checkout has ordinal `0`; the first linked worktree has ordinal `1`, the second has ordinal `2`, and so on. For the first proposal, add the linked-worktree ordinal to each baseline port.
|
|
26
|
+
|
|
27
|
+
| Setting | Baseline | First-proposal rule |
|
|
28
|
+
| --------------------- | -------: | --------------------------------- |
|
|
29
|
+
| `SERVER_LISTEN_PORT` | `7102` | `7102 + linked-worktree ordinal` |
|
|
30
|
+
| `DEV_SERVER_PORT` | `9000` | `9000 + linked-worktree ordinal` |
|
|
31
|
+
| `DEV_SERVER_HMR_PORT` | `24679` | `24679 + linked-worktree ordinal` |
|
|
32
|
+
|
|
33
|
+
`APP_NAME` is the current linked worktree directory name. `API_BASE_URL` is regenerated as `http://localhost:<SERVER_LISTEN_PORT>`.
|
|
27
34
|
|
|
28
|
-
|
|
35
|
+
Every valid explicit setup proposes the same complete tuple and both local files:
|
|
29
36
|
|
|
30
|
-
|
|
37
|
+
```dotenv
|
|
38
|
+
# vona/env/.env.local
|
|
39
|
+
APP_NAME = cabloy-worktree-name
|
|
40
|
+
SERVER_LISTEN_PORT = 7103
|
|
41
|
+
```
|
|
31
42
|
|
|
32
43
|
```dotenv
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
44
|
+
# zova/env/.env.local
|
|
45
|
+
APP_NAME = cabloy-worktree-name
|
|
46
|
+
API_BASE_URL = http://localhost:7103
|
|
47
|
+
DEV_SERVER_PORT = 9001
|
|
48
|
+
DEV_SERVER_HMR_PORT = 24680
|
|
37
49
|
```
|
|
38
50
|
|
|
39
|
-
|
|
51
|
+
The user-facing summary is:
|
|
52
|
+
|
|
53
|
+
> 环境隔离信息:Vona 开发 + Zova 开发
|
|
54
|
+
|
|
55
|
+
If the proposal is unsuitable, say **“再换一批”**. The skill increases every listener port by exactly `+1`, regenerates `API_BASE_URL` from the new Vona port, and presents the next tuple for confirmation. It never writes during this step.
|
|
56
|
+
|
|
57
|
+
This deterministic scheme is not a port reservation or a live collision check. If an application later reports that a port is occupied, request another batch before setup or create another linked worktree; successful application startup remains the final authority.
|
|
58
|
+
|
|
59
|
+
## Configuration boundaries
|
|
60
|
+
|
|
61
|
+
After final confirmation, the skill always writes the two shown files. Keep these invariants:
|
|
40
62
|
|
|
41
63
|
- Vona and Zova use the same unique `APP_NAME`.
|
|
42
|
-
- `API_BASE_URL` points to the
|
|
43
|
-
- `SERVER_LISTEN_PORT`, `DEV_SERVER_PORT`, and `DEV_SERVER_HMR_PORT` are unique
|
|
64
|
+
- `API_BASE_URL` points to the generated Vona `SERVER_LISTEN_PORT`.
|
|
65
|
+
- `SERVER_LISTEN_PORT`, `DEV_SERVER_PORT`, and `DEV_SERVER_HMR_PORT` are unique within the generated worktree tuple.
|
|
66
|
+
- The skill writes no settings beyond the five shown assignments and creates no flavor-specific local file.
|
|
67
|
+
|
|
68
|
+
Admin and Web are alternative commands that share the generated Zova environment. Do not run both frontend development commands concurrently in one worktree. Configure another linked worktree for concurrent development of the other flavor.
|
|
69
|
+
|
|
70
|
+
The enabled Zova Quasar extension maps `DEV_SERVER_HMR_PORT` to Vite's client HMR/WebSocket listener. It is a separate listener when it differs from `DEV_SERVER_PORT`, so every concurrently running Zova development worktree needs its own unique HMR port.
|
|
71
|
+
|
|
72
|
+
For privacy, the skill writes only to absent or empty permitted broad local files. If either target already contains content, it stops without reading or changing either file; manage the existing local configuration outside this workflow or use a fresh linked worktree.
|
|
44
73
|
|
|
45
|
-
|
|
74
|
+
## Edition-aware commands
|
|
75
|
+
|
|
76
|
+
Use commands from the active repository root.
|
|
77
|
+
|
|
78
|
+
| Edition | Marker | Frontend command | Managed clean E2E |
|
|
79
|
+
| ------------ | ------------------ | -------------------------------------------------- | ------------------------------ |
|
|
80
|
+
| Cabloy Basic | `__CABLOY_BASIC__` | `npm run dev:zova:admin` or `npm run dev:zova:web` | `npm run test:e2e:basic:clean` |
|
|
81
|
+
| Cabloy Start | `__CABLOY_START__` | `npm run dev:zova:admin` or `npm run dev:zova:web` | `npm run test:e2e:start:clean` |
|
|
82
|
+
|
|
83
|
+
Run one frontend command, not both, in each worktree. Both managed clean E2E workflows read Vona's effective local `SERVER_LISTEN_PORT`, then reset, start, and target that local runtime.
|
|
46
84
|
|
|
47
85
|
## What this isolates
|
|
48
86
|
|
|
49
87
|
A unique `APP_NAME` separates the application identity used by ordinary framework-managed local resources, including managed test database names and Redis-related key prefixes. Worktree-local generated files, runtime files, build output, and coverage output are already separated by their worktree paths.
|
|
50
88
|
|
|
51
|
-
The
|
|
89
|
+
The generated Vona, Zova development HTTP, and Zova HMR ports let worktrees run their ordinary local processes concurrently. The matching Zova API URL prevents the frontend in one worktree from calling the Vona process in another.
|
|
52
90
|
|
|
53
91
|
This is sufficient for the normal local workflows:
|
|
54
92
|
|
|
55
93
|
```bash
|
|
56
94
|
npm run dev
|
|
95
|
+
# Run one frontend process in this worktree:
|
|
57
96
|
npm run dev:zova:admin
|
|
97
|
+
# or
|
|
58
98
|
npm run dev:zova:web
|
|
59
99
|
npm run test
|
|
60
|
-
npm run test:e2e:basic:clean
|
|
61
100
|
```
|
|
62
101
|
|
|
63
|
-
|
|
102
|
+
## Resources requiring separate design
|
|
64
103
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
Do not add isolation settings preemptively. Extend the local overrides only when the selected workflow actually shares one of these resources:
|
|
104
|
+
Do not extend this universal five-value setup with additional listener or provider settings. An explicit separate design is required for:
|
|
68
105
|
|
|
69
106
|
- an explicitly named database or a separately configured Redis target
|
|
70
107
|
- mail, payment, webhook, object-storage, or other external providers
|
|
71
|
-
-
|
|
72
|
-
- an SSR preview or production process, using `SSR_PROD_PORT`
|
|
108
|
+
- standalone mock-build, SSR preview, or production listeners
|
|
73
109
|
|
|
74
110
|
For example, `APP_NAME` namespaces ordinary framework Redis keys, but it does not create a separate Redis server or automatically isolate custom, unprefixed keys. Likewise, an explicitly configured external database remains shared until it is configured separately.
|
|
75
111
|
|
|
76
|
-
##
|
|
112
|
+
## Guided setup and initialization
|
|
113
|
+
|
|
114
|
+
For confirmation-gated setup, explicitly invoke `/cabloy-worktree-environment`. The skill validates the linked worktree and edition, derives a secret-safe proposal from Git metadata and fixed port baselines, and writes both allowed broad local files after final confirmation.
|
|
77
115
|
|
|
78
|
-
|
|
116
|
+
After local overrides are validated, decide separately whether to run `npm run init`. It is not an environment allocator: it installs dependencies and runs generation/build-related work. When it runs from a linked Git worktree rooted at the project, it preserves the tracked base `APP_NAME` defaults in `vona/env/.env` and `zova/env/.env`; the generated broad `.env.local` files supply the worktree-specific identity. Primary-checkout and non-Git project initialization may still establish base `APP_NAME` from the project directory.
|
|
79
117
|
|
|
80
118
|
## Read together with
|
|
81
119
|
|
|
@@ -297,6 +297,7 @@ For most real business scenarios, the correct answer is to create the suite firs
|
|
|
297
297
|
This guide should be read together with:
|
|
298
298
|
|
|
299
299
|
- [Package Map](/reference/package-map)
|
|
300
|
+
- [Vona Module Dependencies](/backend/module-dependencies)
|
|
300
301
|
- [Backend Directory Structure](/reference/backend-directory-structure)
|
|
301
302
|
- [Frontend Directory Structure](/reference/frontend-directory-structure)
|
|
302
303
|
- [Backend CLI](/backend/cli)
|
|
@@ -81,6 +81,7 @@ Use this package map together with:
|
|
|
81
81
|
- [Suites and Modules](/fullstack/suites-and-modules)
|
|
82
82
|
- [Backend Essentials](/backend/backend-essentials)
|
|
83
83
|
- [Backend Foundation](/backend/foundation)
|
|
84
|
+
- [Vona Module Dependencies](/backend/module-dependencies)
|
|
84
85
|
- [Backend CLI](/backend/cli)
|
|
85
86
|
- [Backend Scripts](/backend/scripts)
|
|
86
87
|
- [Backend Directory Structure](/reference/backend-directory-structure)
|
|
@@ -796,6 +796,95 @@ test(
|
|
|
796
796
|
},
|
|
797
797
|
);
|
|
798
798
|
|
|
799
|
+
test(
|
|
800
|
+
'ATP-SPC-01: Coupon Template renders semantic Admin field controls',
|
|
801
|
+
{ tag: ['@admin', '@flow', '@fia'] },
|
|
802
|
+
async ({ browser }) => {
|
|
803
|
+
test.setTimeout(60_000);
|
|
804
|
+
const adminContext = await browser.newContext();
|
|
805
|
+
const adminPage = await adminContext.newPage();
|
|
806
|
+
const adminPageErrors = collectPageErrors(adminPage);
|
|
807
|
+
try {
|
|
808
|
+
await adminPage.setViewportSize({ width: 1440, height: 900 });
|
|
809
|
+
await login(adminPage, '/commerce-admin/', 'admin', '123456', 'commerceAdmin');
|
|
810
|
+
await adminPage.goto(
|
|
811
|
+
'/commerce-admin/rest/resource/commerce-promotion%3AcouponTemplate/create',
|
|
812
|
+
{
|
|
813
|
+
waitUntil: 'load',
|
|
814
|
+
},
|
|
815
|
+
);
|
|
816
|
+
await expect(adminPage).toHaveURL(
|
|
817
|
+
/\/commerce-admin\/rest\/resource\/commerce-promotion(?:%3A|:|%253A)couponTemplate\/create(?:[/?#]|$)/,
|
|
818
|
+
);
|
|
819
|
+
|
|
820
|
+
for (const groupName of [
|
|
821
|
+
'Basic Information',
|
|
822
|
+
'Discount Policy',
|
|
823
|
+
'Validity Window',
|
|
824
|
+
'Usage Limits',
|
|
825
|
+
]) {
|
|
826
|
+
await expect(adminPage.getByRole('group', { name: groupName })).toBeVisible();
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
const state = adminPage
|
|
830
|
+
.getByRole('group', { name: 'Template State *' })
|
|
831
|
+
.getByRole('combobox');
|
|
832
|
+
await expect(state).toBeVisible();
|
|
833
|
+
await state.selectOption({ label: 'Active' });
|
|
834
|
+
await expect(state).toHaveValue('active');
|
|
835
|
+
|
|
836
|
+
const discountInput = adminPage
|
|
837
|
+
.getByRole('group', { name: 'Fixed Discount *' })
|
|
838
|
+
.getByRole('textbox');
|
|
839
|
+
const minSpendInput = adminPage
|
|
840
|
+
.getByRole('group', { name: 'Minimum Spend *' })
|
|
841
|
+
.getByRole('textbox');
|
|
842
|
+
await discountInput.fill('12.34');
|
|
843
|
+
await minSpendInput.fill('45.67');
|
|
844
|
+
await expect(discountInput).toHaveValue('12.34');
|
|
845
|
+
await expect(minSpendInput).toHaveValue('45.67');
|
|
846
|
+
await expect(
|
|
847
|
+
adminPage.getByRole('group', { name: 'Valid From *' }).locator('input[type="date"]'),
|
|
848
|
+
).toBeVisible();
|
|
849
|
+
await expect(
|
|
850
|
+
adminPage.getByRole('group', { name: 'Valid Until *' }).locator('input[type="date"]'),
|
|
851
|
+
).toBeVisible();
|
|
852
|
+
|
|
853
|
+
await adminPage
|
|
854
|
+
.getByRole('group', { name: 'Name *' })
|
|
855
|
+
.getByRole('textbox')
|
|
856
|
+
.fill('E2E Coupon Template');
|
|
857
|
+
await adminPage.getByRole('group', { name: 'Currency *' }).getByRole('textbox').fill('USD');
|
|
858
|
+
await adminPage
|
|
859
|
+
.getByRole('group', { name: 'Valid From *' })
|
|
860
|
+
.getByRole('textbox')
|
|
861
|
+
.fill('2026-08-04');
|
|
862
|
+
await adminPage
|
|
863
|
+
.getByRole('group', { name: 'Valid Until *' })
|
|
864
|
+
.getByRole('textbox')
|
|
865
|
+
.fill('2026-12-31');
|
|
866
|
+
await adminPage
|
|
867
|
+
.getByRole('group', { name: 'Total Issue Limit' })
|
|
868
|
+
.getByRole('textbox')
|
|
869
|
+
.fill('10');
|
|
870
|
+
await adminPage
|
|
871
|
+
.getByRole('group', { name: 'Total Usage Limit' })
|
|
872
|
+
.getByRole('textbox')
|
|
873
|
+
.fill('10');
|
|
874
|
+
await adminPage
|
|
875
|
+
.getByRole('group', { name: 'Per-customer Issue Limit' })
|
|
876
|
+
.getByRole('textbox')
|
|
877
|
+
.fill('1');
|
|
878
|
+
|
|
879
|
+
await expect(adminPage.getByRole('button', { name: 'Submit', exact: true })).toBeVisible();
|
|
880
|
+
await expect(adminPage.getByRole('button', { name: 'Back', exact: true })).toBeVisible();
|
|
881
|
+
expect(adminPageErrors).toEqual([]);
|
|
882
|
+
} finally {
|
|
883
|
+
await adminContext.close().catch(() => {});
|
|
884
|
+
}
|
|
885
|
+
},
|
|
886
|
+
);
|
|
887
|
+
|
|
799
888
|
test(
|
|
800
889
|
'Commerce Cart: anonymous browser is redirected to login',
|
|
801
890
|
{ tag: ['@web', '@cart'] },
|