Skip to content

[IP-481]: merge duplicate clients#580

Draft
nielsdrost7 wants to merge 1 commit into
InvoicePlane:developfrom
underdogg-forks:feature/481-merge-clients
Draft

[IP-481]: merge duplicate clients#580
nielsdrost7 wants to merge 1 commit into
InvoicePlane:developfrom
underdogg-forks:feature/481-merge-clients

Conversation

@nielsdrost7

@nielsdrost7 nielsdrost7 commented Jul 4, 2026

Copy link
Copy Markdown
Collaborator

Summary

A Merge clients bulk action on the company-panel clients list. Select exactly two clients, pick which one to keep, and every record of the duplicate is moved onto the primary before the duplicate is soft-deleted.

RelationMergeService (Modules/Clients/Services)

merge(Relation $primary, Relation $duplicate): Relation, inside a DB transaction:

  • Guards — throws InvalidArgumentException (translated) when the two records are the same or belong to different companies.
  • Re-parents onto the primary, using the verified FK columns:
    • invoices.customer_id, quotes.prospect_id, payments.customer_id
    • expenses.vendor_id and expenses.customer_id (both FK relations)
    • tasks.customer_id, projects.customer_id, contacts.relation_id
    • morphs: addresses.addressable, communications.communicationable, notes.notable
    • Updates run withoutGlobalScopes() so trashed invoices/quotes and records outside the current tenant scope move too (the same-company guard has already run).
  • Gap-fills the primary: trading_name, unique_name, id_number, coc_number, vat_number, currency_code, language, primary_contact_id are copied from the duplicate only where the primary's value is empty.
  • Soft-deletes the duplicate.

Soft deletes on relations

New migration 2026_07_04_000001_add_soft_deletes_to_relations_table adds nullable deleted_at; Relation now uses the SoftDeletes trait. Schema check: the relations table has no unique indexes (unique_name is just a nullable column), so soft-deleted rows cannot collide with new inserts. One behavioural note: RelationService::generateRelationNumber() derives the next number from the latest non-trashed relation, so a number belonging to a merged-away duplicate could in principle be reissued — there is no DB unique constraint on relation_number, so nothing breaks, but worth knowing.

Bulk action (RelationsTable)

Added only inside the existing ->toolbarActions() BulkActionGroup to keep the conflict surface with #221 minimal (row-action group untouched). Gated on Permission::EDIT_RELATIONS. Validates that exactly two records are selected (danger notification otherwise), shows a radio to choose the primary, calls the service, and notifies success/failure.

Closes

Closes #481 — merge duplicate clients (bulk action + service re-parenting all dependent records + soft delete)

Follow-up issues (not resolved here — reopen if auto-closed on merge)

Closes #589 — Field-by-field conflict picker for client merge (out of scope for #481's initial cut; currently the primary always wins on conflicts)

Tests

Modules/Clients/Tests/Feature/RelationMergeTest.php — 8 PHPUnit tests: re-parenting of every dependent record type (incl. morphs and notes), relations visible under the primary after merge, gap-fill without overwrite, assertSoftDeleted on the duplicate, both guard clauses, the Livewire bulk action happy path, and the not-exactly-two-selected rejection.

CI note

Upstream CI is not running; validated locally (PHP 8.3, sqlite in-memory) against the origin/develop baseline:

  • Baseline full suite: 272 passed / 0 failed / 9 incomplete / 26 skipped
  • This branch: 280 passed / 0 failed / 9 incomplete / 26 skipped (315 total = baseline + 8 new tests, no regressions; the pre-existing "No tests found in QuoteWasUpdatedListenerTest" warning is unchanged)

@InvoicePlane InvoicePlane deleted a comment from coderabbitai Bot Jul 4, 2026
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@nielsdrost7 nielsdrost7 force-pushed the feature/481-merge-clients branch from 1e74eaa to d043201 Compare July 4, 2026 12:23
@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5eae66ce-ba88-41b2-8694-b166f9d02e2c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

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.

[Clients]: Field-by-field conflict picker for client merge [Clients]: Merge duplicate clients

1 participant