Skip to content

fix(admin-features): make admin writes use the field names consumers actually read - #11

Merged
nathanieluriri merged 16 commits into
masterfrom
claude/admin-feature-canonical-schemas
Aug 3, 2026
Merged

nathanieluriri merged 16 commits into
masterfrom
claude/admin-feature-canonical-schemas

Conversation

@nathanieluriri

@nathanieluriri nathanieluriri commented Aug 2, 2026 •

Copy link
Copy Markdown
Owner

Why

The admin console has been writing snake_case field names that no consumer in this repo reads. Traced through the services, not inferred:

Console wrote This repo reads Consequence
price_minor num(d.price) ?? 0 — catalog-service.ts:139 Add-ons priced at 0
discount_value discountValue ?? value ?? percentage, else 0 — promotion-service.ts:44 Promo codes applied 0% discount
is_active isAvailable ?? active, absent ⇒ true — catalog-service.ts:71 Active toggle inert — deactivated items stayed live to customers
display_name title ?? name, fallback 'Service' / 'Cleaning' Services shown as literally "Service" to customers, "Cleaning" to cleaners

Root cause: FeatureCreate/FeatureUpdate were z.object({}).passthrough() with a standing TODO: replace the passthrough shapes. Nine features ran through that factory, so the API accepted arbitrary JSON and nothing ever failed loudly.

What changed

crudRouter accepts optional per-feature schemas. Absent ⇒ today's passthrough, so the four untouched features (cleaner-tags, availability-overrides, payout-adjustments, chat-interventions) behave identically. Five features now validate.

Canonical schemas for the three collections that have consumers — service_definitions, addon_catalog, promo_code — using the exact field names those consumers already read. price is now required (a priceless add-on was the bug); discountValue is required and bounded.

Validation-only schemas for service_area_boundary and dynamic_pricing_rule, keeping their existing snake_case names. Deliberate: neither collection has any consumer in this repo, so there is no canonical vocabulary to migrate toward and renaming would be churn.

An idempotent migration for documents already stored with legacy names, plus a --census mode.

docs/migration/07-domain-endpoints.md records this as deliberate change #6, per app/CLAUDE.md.

Things worth a reviewer's attention

  • Unknown keys are stripped, not rejected. Plain z.object — not .passthrough() (which would let the drift back in), not .strict() (which would 422 an old client mid-rolling-deploy). A client sending legacy fields gets a 200 with them dropped.
  • rule_type is deliberately NOT an enum. It's z.string().min(1), with COMMON_PRICING_RULE_TYPES exported purely as a UI suggestion list. An earlier draft constrained it to four values taken from the admin form's placeholder text — with no consumer anchoring them, that would have rejected PATCHes of legitimately-stored records, silently blocking edits.
  • PromoCodeUpdate re-applies its refines. Built from the raw fields object it carried neither, so a PATCH could set a promo to 500% that POST rejects. The bare-discountValue case takes the conservative branch (reject unless the patch explicitly says FIXED), documented in a comment and pinned by tests.
  • serviceId is nullable. delete-ing a key on a PATCH never clears it, so un-scoping an add-on was a silent no-op; the console now sends explicit null.
  • tests/admin-payload-reaches-consumers.test.ts asserts the CURRENT BROKEN behaviour on purpose. The legacy cases pin that a legacy doc yields title "Service", a null price, an add-on price of 0, and stays visible when is_active: false. They are the executable definition of what the migration must repair — and the only record of why the alias chains (title ?? name, basePrice ?? price) exist. Please don't delete them as "asserting a bug".

The migration — read before running

app/server/scripts/migrate-admin-feature-fields.ts. It has not been run against any database.

Intended order is census → dry-run → apply:

npx tsx server/scripts/migrate-admin-feature-fields.ts --census    # read-only, lists real field names + counts
npx tsx server/scripts/migrate-admin-feature-fields.ts             # dry-run (the default)
npx tsx server/scripts/migrate-admin-feature-fields.ts --apply     # writes

Safety properties, each covered by tests:

  • Dry-run by default. Exactly one write call in the file, gated behind if (apply). A typo'd flag falls through to read-only.
  • Idempotent — a re-run makes no second change, so an interrupted run is safe to repeat.
  • Never clobbers — a document already holding a canonical value keeps it.
  • Never writes NaN — non-finite conversions are reported, not stored.
  • Legacy keys are not removed ($set only, no $unset) so a bad mapping stays auditable. A cleanup pass should be a separate, later script.

Two deliberate omissions:

  • base_duration_minutes is counted and reported, never migrated. Base duration is not a pricing floor; mapping it to minimumHours would silently change customer quotes on hourly services. Flagged for a human decision.
  • The mapping table is not yet proven complete. It was derived from the admin form's fields, not from stored data — which is what --census is for. Run it before --apply.

Companion frontend PR

The matching console work is in a separate repository: nathanieluriri/jade-guardian PR #3. That one rebuilds the five Operations Core forms on this vocabulary.

Deploy order matters, and I understated this originally. I first wrote that the old console "keeps working because unknown keys are stripped". That is wrong in an important way: stripping means an older console build sending display_name / is_active / price_minor / discount_value gets a 200 while its edit is silently discarded. Nothing errors — the admin just watches their change fail to take effect.

So: deploy the admin console first, then this. Merging while an old console build is live turns a visible bug into an invisible one. Caught by CodeRabbit; the 07-domain-endpoints.md entry now says the same thing.

Verification

  • npm test — 463 passing, 46 files
  • npm run typecheck — clean
  • npm run lint — clean (one pre-existing unrelated warning)

Summary by CodeRabbit

  • New Features

    • Added validated create and update workflows for service definitions, add-ons, promo codes, service areas, and pricing rules.
    • Standardized request fields, pricing formats, discount validation, date checks, and service-area boundary data.
    • Added support for safe migration of legacy admin-feature fields, including conflict detection and review reporting.
  • Documentation

    • Documented endpoint validation rules, canonical field formats, and migration dry-run and apply modes.
  • Tests

    • Added coverage for validation, migration behavior, and request handling.

…Router

Absent schemas keep the historical passthrough behaviour so the nine
existing features migrate one at a time.
…o correct routes

Replace weak toBeDefined() checks with OpenAPI-document introspection
so a POST/PATCH schema swap or wrong default fails the test.
Admin writes now use the field names catalog/pricing/availability actually
read. Legacy snake_case keys are stripped rather than stored.
price is now required and in major units; the old price_minor key meant
every admin-created add-on priced at zero.
discountValue is required and bounded; `active` replaces the inert
`is_active`, which promotion-service never read.
PromoCodeUpdate previously dropped both refine rules from
PromoCodeCreate, allowing a PATCH to set a >100% discount or an
inverted date range. Reapply both rules to the partial schema;
percent cap uses the conservative policy - a discountValue >100 is
rejected unless discountType === 'FIXED' is explicit in the patch.
…g rules

Field names left as-is: neither collection has a backend consumer, so
there is no canonical vocabulary to migrate toward.
The closed enum was speculative - sourced from admin-form placeholder
text, not a backend contract, since dynamic_pricing_rule has no
consumer. It risked rejecting PATCHes of legitimately stored records.
COMMON_PRICING_RULE_TYPES is kept as a plain list for the UI's guided
picker (Task 12); the API no longer enforces it.
Canonical payloads render correctly; legacy snake_case payloads render as
'Service' with null price, stay visible when deactivated, and price add-ons
at zero. The legacy cases define what the migration must repair.
Pure mapping functions plus a dry-run-by-default runner. Never clobbers a
document that already carries canonical fields.
Add-ons now migrate the same title/isAvailable/description fields as
services (a disabled legacy add-on no longer goes live at a real
charge). Drop the base_duration_minutes -> minimumHours mapping since
it silently changes pricing floors; flag it for human review instead.
Normalize legacy promo codes. Add a read-only --census runner mode,
and split the summary into migrated/skipped/conflicting/unrecognised
so a genuine legacy-vs-canonical contradiction is never silently
dropped. Guard every numeric conversion against NaN.
…flict

A document containing both a conflicting field and a cleanly-migratable
one is still withheld entirely, but the summary now names the withheld
keys per document so an operator knows to re-run after resolving.
…g add-on serviceId

migratePromoCode uppercased discount_type verbatim, so the console's old
"percentage" placeholder became "PERCENTAGE" which the strict
z.enum(['PERCENT','FIXED']) schema rejects -- migrated promos rendered a
blank radio and any PATCH carrying the field 422'd. Map known legacy
spellings explicitly and flag anything else for human review instead of
writing a value the schema will never accept.

Also widen AddOnCreate.serviceId to accept null so the admin console can
explicitly clear an add-on's service scope on PATCH; omitting the key (the
previous approach) left it untouched server-side.
@vercel

vercel Bot commented Aug 2, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
marcus-cleaning-backend Ready Ready Preview Aug 2, 2026 2:33pm

@coderabbitai

coderabbitai Bot commented Aug 2, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds canonical schemas for five admin-feature resources, wires them into CRUD request validation and OpenAPI definitions, and adds pure migration helpers plus a MongoDB census, dry-run, and apply CLI.

Changes

Admin feature contracts and migration

Layer / File(s) Summary
Resource schemas and validation
app/server/schemas/admin-features.ts, app/tests/admin-service-definition-schema.test.ts, app/tests/admin-addon-schema.test.ts, app/tests/admin-promo-schema.test.ts, app/tests/admin-area-and-rule-schema.test.ts, docs/migration/07-domain-endpoints.md
Added canonical create and update schemas with field validation, normalization, unknown-field stripping, OpenAPI names, inferred types, and migration documentation.
CRUD schema selection and routing
app/server/routes/admin-features/_crud.ts, app/server/routes/admin-features/index.ts, app/tests/admin-feature-crud-schema.test.ts
CRUD routers now use configured resource schemas for POST and PATCH requests, with shared-schema fallbacks when configuration is omitted.
Pure legacy-field migration
app/server/services/admin-feature-migration.ts, app/tests/admin-feature-migration.test.ts
Added service-definition, add-on, and promo-code migration functions with normalization, conflict detection, canonical-value preservation, withheld patches, and idempotence.
Migration execution and reporting
app/server/scripts/migrate-admin-feature-fields.ts, app/tests/admin-payload-reaches-consumers.test.ts
Added collection scanning, census output, dry-run reporting, explicit apply writes, and consumer regression coverage for canonical and legacy payloads.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant AdminFeatureMigrationCLI as migrate-admin-feature-fields
  participant MongoDB
  participant migrateServiceDefinition
  participant migrateAddOn
  participant migratePromoCode
  AdminFeatureMigrationCLI->>MongoDB: scan configured collections
  AdminFeatureMigrationCLI->>migrateServiceDefinition: migrate service definition document
  AdminFeatureMigrationCLI->>migrateAddOn: migrate add-on document
  AdminFeatureMigrationCLI->>migratePromoCode: migrate promo code document
  migrateServiceDefinition-->>AdminFeatureMigrationCLI: migration result
  migrateAddOn-->>AdminFeatureMigrationCLI: migration result
  migratePromoCode-->>AdminFeatureMigrationCLI: migration result
  AdminFeatureMigrationCLI->>MongoDB: apply safe $set patch when --apply is enabled
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: admin writes now use the canonical field names consumed by backend services.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/admin-feature-canonical-schemas

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (7)
app/server/services/admin-feature-migration.ts (2)

161-187: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider covering the name and active legacy spellings.

listServices and listServiceExtras in app/server/services/catalog-service.ts (lines 71-90 and 126-142) already fall back to d.name and d.active. Documents that carry those keys are therefore not broken for customers, but the migration does not add them to knownKeys. Such documents count as unrecognised in the runner summary, which makes the census harder to read. Run --census first and confirm whether name and active exist in the real collections.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/server/services/admin-feature-migration.ts` around lines 161 - 187, Check
the real collections with --census to confirm whether legacy name and active
keys exist, then update migrateServiceDefinition and migrateAddOn to map those
spellings to title and isAvailable and include them in knownKeys handling,
matching the existing display_name and is_active mappings.

205-222: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

discount_type is flagged even when the canonical value already exists.

If a document has both discount_type: 'buy_one_get_one' and discountType: 'PERCENT', line 208 still pushes discount_type into flagged. The runner then counts that document under "needs human decision" at line 202. Nothing would be written for that field in either case, because the canonical value is present and always wins. The report asks an operator to act on a document that needs no action.

Consider flagging only when discountType is absent.

♻️ Proposed change
   if ('discount_type' in doc) {
     const normalized = normalizeLegacyDiscountType(doc.discount_type)
-    if (normalized === null) {
+    if (normalized === null) {
+      if (!('discountType' in doc)) flagForHumanReview(doc, 'discount_type', ctx)
+    } else if ('discountType' in doc) {

Note: app/tests/admin-feature-migration.test.ts lines 146-156 pin the current behavior, so update that test together with this change.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/server/services/admin-feature-migration.ts` around lines 205 - 222,
Update the discount_type handling around normalizeLegacyDiscountType so an
invalid legacy value is flagged only when the canonical discountType field is
absent; when discountType exists, preserve conflict detection for differing
values and avoid adding the document to human review. Update the corresponding
admin-feature-migration test expectations to reflect this behavior.
app/tests/admin-feature-migration.test.ts (1)

26-34: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add a conflict assertion to these two cases.

Both cases produce a conflict in mapField (lines 114-119 of app/server/services/admin-feature-migration.ts), not a silent skip. The tests assert only that patch stays empty. A regression that drops conflict reporting for service definitions would still pass.

💚 Proposed extra assertions
   it('maps notes to description only when description is absent', () => {
     expect(migrateServiceDefinition({ notes: 'n' }).patch.description).toBe('n')
-    expect(migrateServiceDefinition({ notes: 'n', description: 'd' }).patch.description).toBeUndefined()
+    const withBoth = migrateServiceDefinition({ notes: 'n', description: 'd' })
+    expect(withBoth.patch.description).toBeUndefined()
+    expect(withBoth.conflicts).toHaveLength(1)
   })
 
   it('never clobbers an existing canonical title', () => {
-    const { patch } = migrateServiceDefinition({ display_name: 'Legacy', title: 'Canonical' })
+    const { patch, conflicts } = migrateServiceDefinition({ display_name: 'Legacy', title: 'Canonical' })
     expect(patch.title).toBeUndefined()
+    expect(conflicts).toHaveLength(1)
   })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/tests/admin-feature-migration.test.ts` around lines 26 - 34, Add
assertions in both migration tests for the conflict reported by mapField when
notes and description, or display_name and title, conflict. Verify the
migrateServiceDefinition result preserves the expected conflict entry while
retaining the existing patch assertions.
app/server/scripts/migrate-admin-feature-fields.ts (2)

92-125: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider isolating per-document write failures.

A single updateOne rejection propagates out of runCollection, out of main, and into the catch at line 230. The process then exits with code 1. The summary for the collections already processed is never printed, and the operator cannot tell how far the run got. The migration is idempotent, so a partial run is recoverable, but only if the operator knows what happened.

Catch the error per document, record the _id in a failedIds list, and report it in the summary.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/server/scripts/migrate-admin-feature-fields.ts` around lines 92 - 125,
Isolate per-document failures in the updateOne call within the runCollection
loop so one rejected write does not abort processing or suppress the completed
summary. Catch the error for each document, record its _id in a failedIds list,
and extend the summary/reporting output to include those failed document IDs
while preserving existing migration counters.

183-186: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Report the _ids for invalid documents.

conflicting documents are listed by _id at line 188, so an operator can find and fix them. invalid documents get only a count. Line 96 discards result.invalid, which holds the offending legacy key names. The operator cannot locate a document with a malformed numeric field, so those documents stay unmigrated on every subsequent run with no way to act on the report.

Record the _id and the invalid field names, then print them like the conflict list.

♻️ Proposed reporting change
 interface CollectionSummary {
   ...
   conflictIds: string[]
+  /** _id -> legacy keys whose converted value was non-finite. */
+  invalidByDocId: Record<string, string[]>
-    if (result.invalid.length > 0) summary.invalid += 1
+    if (result.invalid.length > 0) {
+      summary.invalid += 1
+      summary.invalidByDocId[String(doc._id)] = result.invalid
+    }
     if (s.conflictIds.length > 0) {
       console.log(`    conflicting _ids: ${s.conflictIds.join(', ')}`)
     }
+    for (const [id, keys] of Object.entries(s.invalidByDocId)) {
+      console.log(`    invalid _id ${id}: non-numeric ${keys.join(', ')}`)
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/server/scripts/migrate-admin-feature-fields.ts` around lines 183 - 186,
Update the invalid-document handling in the migration flow to retain each
invalid document’s _id and offending legacy field names instead of discarding
result.invalid. Extend the summary/reporting logic near the existing
conflicting-document list to print the invalid entries in the same actionable
format, while preserving the current invalid count.
app/tests/admin-payload-reaches-consumers.test.ts (1)

67-86: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Give this block an explicit removal trigger.

These tests assert defects as expected behavior. Line 78 asserts that a service marked is_active: false is still shown to customers. The fixtures are hardcoded, so the block stays green after the migration repairs the real data. Nothing signals that the block is obsolete.

Two consequences:

  1. The block never fails, so it cannot tell anyone when the damage is gone.
  2. If someone later adds a display_name or is_active fallback to listServices in app/server/services/catalog-service.ts, line 71 and line 78 fail. A reader without context may revert the fallback instead of deleting the test.

Add a comment that names the tracking issue and the condition for deleting the block. State that the fix belongs in the migration, not in a consumer fallback.

Do you want me to open a follow-up issue to remove this block after --apply runs in production?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/tests/admin-payload-reaches-consumers.test.ts` around lines 67 - 86, Add
a clear comment immediately before the legacy admin payload test block
identifying the tracking issue and stating that the block must be removed after
the migration’s --apply run completes in production. Clarify that the corrective
fix belongs in the migration, not in consumer fallbacks such as listServices, so
future failures are interpreted correctly.
app/server/schemas/admin-features.ts (1)

103-126: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add a cross-field check tying priceUnit to the matching price field.

ServiceDefinitionCreate lets priceUnit be 'FLAT' or 'HOURLY' while basePrice and hourlyRate stay fully independent optional fields. Nothing stops a client from creating a FLAT service with no basePrice, or an HOURLY service with no hourlyRate. The docstring says this schema exists specifically because services previously "rendered as 'Service' with no price," so this gap can reproduce the same symptom through the new canonical schema.

Confirm with pricing-service.ts whether it tolerates a missing price for the selected unit, or add a refine to require the matching field.

♻️ Proposed refine to require the matching price field
 export const ServiceDefinitionCreate = z
   .object({
     title: z.string().min(1),
     description: z.string().optional(),
     basePrice: z.number().nonnegative().optional(),
     hourlyRate: z.number().nonnegative().optional(),
     minimumHours: z.number().positive().optional(),
     maximumHours: z.number().positive().optional(),
     hourIncrement: z.number().positive().optional(),
     priceUnit: AdminPriceUnit.optional(),
     currency: z.string().min(1).optional(),
     isAvailable: z.boolean().optional(),
     checklist: z.array(z.string()).optional(),
     service_key: z.string().optional(),
   })
+  .refine((v) => v.priceUnit !== 'FLAT' || v.basePrice != null, {
+    message: 'basePrice is required when priceUnit is FLAT',
+    path: ['basePrice'],
+  })
+  .refine((v) => v.priceUnit !== 'HOURLY' || v.hourlyRate != null, {
+    message: 'hourlyRate is required when priceUnit is HOURLY',
+    path: ['hourlyRate'],
+  })
   .openapi('ServiceDefinitionCreate')
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/server/schemas/admin-features.ts` around lines 103 - 126, Add a
cross-field validation refinement to ServiceDefinitionCreate that requires
basePrice when priceUnit is FLAT and hourlyRate when priceUnit is HOURLY, using
the pricing-service.ts contract to confirm the expected behavior. Ensure the
validation targets the relevant price field and preserves existing validation
for all other combinations, including omitted priceUnit.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/server/schemas/admin-features.ts`:
- Around line 103-126: Coordinate deployment rather than changing the schema
definitions: update and deploy the admin console to send the current camelCase
field names before releasing ServiceDefinitionCreate, AddOnCreate,
PromoCodeCreate, and PromoCodeUpdate. Verify the console no longer sends legacy
fields such as display_name, base_duration_minutes, is_active, price_minor, or
discount_value, then ship the schema cutover.

In `@app/server/scripts/migrate-admin-feature-fields.ts`:
- Around line 115-119: Update the updateOne filter in the result.changed/apply
branch to require every canonical field in result.patch to remain absent at
write time, preventing stale legacy values from overwriting concurrent admin
changes. Preserve the existing _id filter and do not include code in the
$exists:false guard because it is normalized in place.
- Around line 228-233: Replace the process.exit calls in the main() completion
chain with process.exitCode assignments so pending I/O can finish. Add a
closeClient helper using the MongoClient from app/server/core/mongo.ts, and
invoke it after successful completion and before setting the failure exit code
in the catch handler.

In `@app/server/services/admin-feature-migration.ts`:
- Around line 131-132: Update toNumber and the mapField conversion flow so only
actual numbers and non-empty, numeric strings are converted; preserve valid zero
values. Ensure null, empty or whitespace strings, arrays, booleans, and other
non-numeric inputs remain invalid and are reported instead of being written as
canonical values.

In `@app/tests/admin-feature-migration.test.ts`:
- Around line 146-156: Update the test description and inline comment in the
migratePromoCode test to match the existing assertions: the unrecognised legacy
discount_type should produce no conflict, leave the canonical value unchanged,
and be flagged. Do not change the implementation or assertions.

---

Nitpick comments:
In `@app/server/schemas/admin-features.ts`:
- Around line 103-126: Add a cross-field validation refinement to
ServiceDefinitionCreate that requires basePrice when priceUnit is FLAT and
hourlyRate when priceUnit is HOURLY, using the pricing-service.ts contract to
confirm the expected behavior. Ensure the validation targets the relevant price
field and preserves existing validation for all other combinations, including
omitted priceUnit.

In `@app/server/scripts/migrate-admin-feature-fields.ts`:
- Around line 92-125: Isolate per-document failures in the updateOne call within
the runCollection loop so one rejected write does not abort processing or
suppress the completed summary. Catch the error for each document, record its
_id in a failedIds list, and extend the summary/reporting output to include
those failed document IDs while preserving existing migration counters.
- Around line 183-186: Update the invalid-document handling in the migration
flow to retain each invalid document’s _id and offending legacy field names
instead of discarding result.invalid. Extend the summary/reporting logic near
the existing conflicting-document list to print the invalid entries in the same
actionable format, while preserving the current invalid count.

In `@app/server/services/admin-feature-migration.ts`:
- Around line 161-187: Check the real collections with --census to confirm
whether legacy name and active keys exist, then update migrateServiceDefinition
and migrateAddOn to map those spellings to title and isAvailable and include
them in knownKeys handling, matching the existing display_name and is_active
mappings.
- Around line 205-222: Update the discount_type handling around
normalizeLegacyDiscountType so an invalid legacy value is flagged only when the
canonical discountType field is absent; when discountType exists, preserve
conflict detection for differing values and avoid adding the document to human
review. Update the corresponding admin-feature-migration test expectations to
reflect this behavior.

In `@app/tests/admin-feature-migration.test.ts`:
- Around line 26-34: Add assertions in both migration tests for the conflict
reported by mapField when notes and description, or display_name and title,
conflict. Verify the migrateServiceDefinition result preserves the expected
conflict entry while retaining the existing patch assertions.

In `@app/tests/admin-payload-reaches-consumers.test.ts`:
- Around line 67-86: Add a clear comment immediately before the legacy admin
payload test block identifying the tracking issue and stating that the block
must be removed after the migration’s --apply run completes in production.
Clarify that the corrective fix belongs in the migration, not in consumer
fallbacks such as listServices, so future failures are interpreted correctly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f87bbdd7-4784-41f4-9803-aaeef5f3a500

📥 Commits

Reviewing files that changed from the base of the PR and between 8031a43 and 75bca4f.

📒 Files selected for processing (13)
  • app/server/routes/admin-features/_crud.ts
  • app/server/routes/admin-features/index.ts
  • app/server/schemas/admin-features.ts
  • app/server/scripts/migrate-admin-feature-fields.ts
  • app/server/services/admin-feature-migration.ts
  • app/tests/admin-addon-schema.test.ts
  • app/tests/admin-area-and-rule-schema.test.ts
  • app/tests/admin-feature-crud-schema.test.ts
  • app/tests/admin-feature-migration.test.ts
  • app/tests/admin-payload-reaches-consumers.test.ts
  • app/tests/admin-promo-schema.test.ts
  • app/tests/admin-service-definition-schema.test.ts
  • docs/migration/07-domain-endpoints.md

Comment thread app/server/schemas/admin-features.ts
Comment thread app/server/scripts/migrate-admin-feature-fields.ts
Comment thread app/server/scripts/migrate-admin-feature-fields.ts
Comment thread app/server/services/admin-feature-migration.ts Outdated
Comment thread app/tests/admin-feature-migration.test.ts Outdated
- toNumber no longer coerces null/''/whitespace/[]/false to 0, so malformed
  legacy price/discount values are reported as invalid instead of migrating
  as a canonical zero
- migration write now re-checks its precondition via buildUpdateFilter,
  guarding against a concurrent admin edit during the long scan-and-write pass
- migration script closes the Mongo client and uses process.exitCode instead
  of process.exit
- recognise and map legacy name/active spellings, matching catalog-service's
  existing read-time fallback
- fix a test whose name/comment contradicted its own assertions
…own keys

Stripping means an older admin-console build gets a 200 while its edit is
silently discarded, not that it keeps working. Console deploys first.
@nathanieluriri
nathanieluriri merged commit a007307 into master Aug 3, 2026
3 checks passed

This branch was successfully deployed

1 active deployment
Preview — 563a0593 Deployed Aug 2, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant