Quotation restriction → Control BEV + staging tree — Implementation Plan
Goal: Staging form uses Sophia draft on version__staging__ S3 (no Control write); publish write-backs Control BEV like the old Booking Engines screen so prod form S3 expands.
Task: TICKET-108481
Handoff: 108481
Spec: none — clarified in-session
Architecture: On Sophia temp-tree build (initTempTree isSophia), overlay company.bookingEngineValidations with rules mapped from Sophia staging (version__staging__{companyId}) before lambda setJsonFormValidations — Control untouched. On PublishDeploymentUseCase, after promoting quotation restriction to prod, map rules→BEV and PUT company on Control (GET+merge+PUT) so Companies.update refresh recompiles prod S3. Discard/restore stays existing Sophia flow; prod Control only changes on publish.
Tech: NestJS sophia-backend (Jest + fakes), generic-bot (Jest colocated *.test.ts), Control PUT /companies/:id, tree-manager setJsonFormValidations (no change if company BEV pre-patched).
Test layout:
- sophia-backend:
npm test -- <path>.spec.ts(Jest,*.spec.tsnext to use-case/provider;FakeControlProviderundersrc/tests/fakes/providers/) - generic-bot:
npx jest --forceExit <path>.test.ts(colocated*.test.ts; avoid fullnpm testcoverage gate per task) Global Constraints: - No Control write on staging save/patch/discard — only overlay staging tree BEV
- Write-back Control only on publish (after prod quotation restriction promote)
- Revert = normal discard (staging) or edit+publish again (prod)
- TDD on every task (red → green)
- Execute via execute (inline only)
- Final review via review (designated reviewer model, mandatory)
- No auto-commit; no push
Rejected simpler options: (1) tree-manager always reads Sophia on compile without Control write-back — leaves Livechat/other BEV readers stale; human chose write-back. (2) write-back Control on every staging patch — advances prod form before publish.
Task 1: Rules → BEV mapper (sophia-backend)
Files:
- Create:
src/modules/quotation-restriction/mappers/quotation-restriction-to-booking-engine-validations.mapper.ts - Test:
src/modules/quotation-restriction/mappers/quotation-restriction-to-booking-engine-validations.mapper.spec.ts - Modify (optional reuse): keep inverse of
CreateQuotationRestrictionUseCaseService.mapBookingEngineValidationsToRules— do not duplicate Control→rules
Interfaces:
- Consumes:
Partial<QuotationRestrictionRules> - Produces:
BookingEngineValidations(same shape Control/BookingEnginesValidationServiceexpect:guestsValidation.rules[ALL],periodValidation.rules[ALL])
Acceptance:
- maxAdults/maxGuests/period fields round-trip with create syncFromControl mapping
- empty/undefined rules → null or empty validations consistent with old screen null-clear behavior (document choice in test)
TDD:
- Red: failing mapper specs for maxAdults + period fields
- Green: pure mapper
- Verify:
npm test -- src/modules/quotation-restriction/mappers/quotation-restriction-to-booking-engine-validations.mapper.spec.ts - Expected: pass
Risk: mech
Task 2: ControlProvider update bookingEngineValidations
Files:
- Modify:
src/providers/control/control.provider.contract.ts - Modify:
src/providers/control/control.provider.ts - Modify:
src/tests/fakes/providers/fake-control.provider.ts - Modify:
src/services/control/control.service.ts - Test:
src/providers/control/control.provider.spec.ts - Test:
src/services/control/control.service.spec.ts
Interfaces:
- Consumes:
companyId,BookingEngineValidations | null - Produces: void via GET
/companies/:id→ setbookingEngineValidations→ PUT/companies/:id(same auth as existing Control calls) ControlService.updateBookingEngineValidations(companyId, validations)wraps provider
Acceptance:
- Fake records last PUT body with updated BEV
- Provider PUT failure throws typed Control exception (mirror lead-config pattern)
- Does not call Control on unrelated fields beyond merge of returned company + BEV
TDD:
- Red: provider/service specs expect update method
- Green: GET+merge+PUT
- Verify:
npm test -- src/providers/control/control.provider.spec.ts src/services/control/control.service.spec.ts - Expected: pass
Risk: integ
Task 3: Publish write-back Control BEV
Files:
- Modify:
src/modules/deployment/use-cases/publish-deployment/publish-deployment.usecase.ts - Test:
src/modules/deployment/use-cases/publish-deployment/publish-deployment.usecase.spec.ts - Modify (thin helper if needed):
src/modules/quotation-restriction/quotation-restriction.service.tsonly if publish needs a namedsyncBookingEngineValidationsToControlto keep use-case thin
Interfaces:
- Consumes: prod quotation restriction rules after
createOrUpdateProdQuotationRestriction - Produces:
ControlService.updateBookingEngineValidations(companyId, mappedBEV) - Order: promote Sophia prod → write-back Control → existing cache invalidate
Acceptance:
- When staging has quotation restriction, publish maps published rules and updates Control
- When no quotation restriction, Control update not called
- Control update failure fails publish (same class as other hard promotions — form would stay wrong if soft-fail)
- Discard/restore paths unchanged (no Control write)
TDD:
- Red: publish specs assert Control update called with mapped maxAdults; assert not called when no restriction; assert publish throws when Control update rejects
- Green: hook after promote
- Verify:
npm test -- src/modules/deployment/use-cases/publish-deployment/publish-deployment.usecase.spec.ts - Expected: pass
Risk: integ
Task 4: Staging temp tree overlays Sophia staging BEV
Files:
- Modify:
src/connectors/test-chat-web/index.ts(or small named helper undersrc/services/quotation-restriction/) - Modify:
src/services/quotation-restriction/QuotationRestrictionService.ts— add named method to resolve validations forversion__staging__{companyId}(API path is versionCompanyId) - Test:
src/services/quotation-restriction/QuotationRestrictionService.test.ts - Test: new or existing connector/helper test colocated if extracted
Interfaces:
- Consumes: prod
company+isSophia===truebeforetreeCompiler.initTempTree - Produces:
company.bookingEngineValidationsset from Sophia staging rules (mapper already in generic-bot); on fetch miss/error keep company BEV (fail-soft so temp tree still builds) - Lambda
init-temp-treeunchanged — already appliescompany.bookingEngineValidationsviasetJsonFormValidations
Acceptance:
- isSophia create-test fetches
version__staging__{sanitize(companyId)}(or equivalent versionCompanyId), maps rules, sets BEV before lambda - Non-Sophia temp tree unchanged
- No Control HTTP from this path
- After discard/restoreToProd on staging restriction, next rebuild uses restored staging rules
TDD:
- Red: QuotationRestrictionService (or helper) test for staging versionCompanyId fetch + overlay; assert Control not involved
- Green: minimal overlay in executeCreateTest when isSophia
- Verify:
npx jest --forceExit src/services/quotation-restriction/QuotationRestrictionService.test.ts - Expected: pass
Risk: integ
Task 5: Publish regression — discard does not touch Control
Files:
- Test:
src/modules/deployment/use-cases/discard-changes/discard-changes.usecase.spec.ts - Modify: none unless a regression spy is missing
Interfaces:
- Consumes: existing discard flow (
restoreToProdQuotationRestriction) - Produces: proof ControlService.updateBookingEngineValidations is never called on discard
Acceptance:
- Discard restores Sophia staging only
- Control BEV remains last published value (no update call)
TDD:
- Red: discard spec with Control update spy → must not be called
- Green: no production code if already true; else remove accidental wiring
- Verify:
npm test -- src/modules/deployment/use-cases/discard-changes/discard-changes.usecase.spec.ts - Expected: pass
Risk: mech