feat(berths): website auto-promote toggle + manual-override soft-pin priority
All checks were successful
Build & Push Docker Images / lint (push) Successful in 2m47s
Build & Push Docker Images / build-and-push (push) Successful in 6m49s

- website_berth_autopromote_enabled (default OFF): a website registration for a
  specific, currently-available berth auto-creates a prospect (client + optional
  yacht + interest) and links the berth is_specific_interest=true, flipping the
  public map to Under Offer; general/residence/contact submissions stay
  capture-only. Marks the submission converted so a rep never double-creates it.
- derivePublicStatus now honours a manual pin (soft pin): a manually-set status
  wins over the interest-derived Under Offer, but a real permanent tenancy or an
  explicit sold still override it.
- berth rules engine respects a manual pin EXCEPT for sale triggers (-> sold),
  so a confirmed sale still wins but soft auto-changes never stomp a pin.
- Reset-to-automatic action (service + API POST /berths/[id]/status/reset + UI)
  to drop a manual pin; lock badge on every manual override (list + detail);
  divergence banner prompting reset when a pinned-Available berth has a deal.
- migration stage map updated to the §4b signed-off mapping: GQI -> enquiry
  unless it named a berth/size marker (-> qualified); SQI -> qualified.

Tests: +public-berths soft-pin cases, +website-intake-promote helpers,
+migration GQI marker rule. 1582 unit/integration green; tsc clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-02 20:10:04 +02:00
parent 04ddd59662
commit 15a139e86f
14 changed files with 802 additions and 19 deletions

View File

@@ -117,6 +117,31 @@ describe('derivePublicStatus', () => {
expect(derivePublicStatus('available', true)).toBe('Under Offer');
});
});
describe('manual override (soft pin)', () => {
it('a manual "available" pin wins over an active specific-interest link', () => {
// The key behaviour: a rep deliberately keeping a berth Available is NOT
// overridden by an interest-derived "Under Offer".
expect(derivePublicStatus('available', true, false, 'manual')).toBe('Available');
});
it('a manual "under_offer" pin shows Under Offer', () => {
expect(derivePublicStatus('under_offer', false, false, 'manual')).toBe('Under Offer');
});
it('a manual "sold" pin shows Sold', () => {
expect(derivePublicStatus('sold', false, false, 'manual')).toBe('Sold');
});
it('a real permanent tenancy still overrides a manual "available" pin (soft pin)', () => {
expect(derivePublicStatus('available', false, true, 'manual')).toBe('Sold');
expect(derivePublicStatus('available', true, true, 'manual')).toBe('Sold');
});
it('"automated" override mode falls through to normal derivation (interest promotes)', () => {
expect(derivePublicStatus('available', true, false, 'automated')).toBe('Under Offer');
});
it('null/omitted override mode preserves pre-pin behaviour', () => {
expect(derivePublicStatus('available', true, false, null)).toBe('Under Offer');
expect(derivePublicStatus('available', true, false)).toBe('Under Offer');
});
});
});
describe('isPermanentTenureType', () => {