Duplicate an event type, with every child dataset - #22
Open
distronode-com wants to merge 2 commits into
Open
Conversation
…alnode#17) POST /v1/event-types/{slug}/duplicate copies an event type and everything that hangs off it - intake questions, host assignments, event-type-specific availability rules, the reminder schedule, and the custom email subjects and notes - inside one transaction, plus a Duplicate action on each row of the event-types list. Three fields are deliberately not inherited: is_active is forced off (a copy exists to be edited, and publishing one on creation is the accident this endpoint has to avoid), archived_at is cleared (archiving belongs to the original's lifecycle), and the slug is regenerated as <slug>-copy, then -copy-2, -copy-3, … because it is UNIQUE across the instance. Everything else is copied verbatim, price_cents and currency included: zeroing a copied price is how a paid meeting quietly starts selling for nothing. Bookings are not copied. A copied booking would be a meeting nobody agreed to, with a live manage link, and it would distort both the per-invitee booking cap and the double-booking guard. Implementation notes: - The row copy is one INSERT … SELECT, so no inherited column's value passes through this process and none can be zeroed in transit. Because SQLite has no "copy every column" form, the column list is explicit - and a drift gate test reads pragma_table_info('event_types') and fails when a column is handled by neither the copy nor the documented not-inherited list, so a future migration cannot silently drop a field out of every copy. - Child rows are read into slices, each cursor closed, before anything is written. The pool is MaxOpenConns(1) (ARCHITECTURE §4), so a write issued while a cursor is open deadlocks on the connection that cursor holds. - Availability rules with event_type_id IS NULL are not copied: those are the host's global default and already apply to the copy. Copying one would promote a global rule to an event-specific one, and the original's later edits would stop reaching the copy. - Owner-scoped like PATCH and DELETE: an assigned host sees an event type read-only, so a copy they could not edit would be worse than a 404. Tests cover each child dataset, fresh child ids, the global-rule exclusion, slug uniqueness against another user's slug and against earlier copies, the archived source case, the non-owner 404, that no bookings are copied, and rollback when a child insert fails (forced with a trigger, asserting no half-built event type survives).
…ules (Calnode#17) Independent re-check of this branch against the issue's requirement list turned up two things asserted only in passing, so they now have tests of their own: - copiesEveryEmailTemplateColumn — the issue names "email/reminder templates" as one of the datasets that must be copied. They are columns on event_types rather than a table, so the big test only sampled the three that appear in the API shape; this compares all nine msg_*/subj_* columns on the copied row against the source, and re-checks the reminder schedule. - isCreatedInactiveWithThePriceIntact — the two requirements that pull in opposite directions (the copy must not be live, its price must not be reset), checked on the row rather than only in the response, plus that the source is left untouched. Verified: gofmt -l . empty, go vet ./... clean, go test ./... exit 0 (26 packages, no failures). The nine duplicate tests pass individually. This branch touches no locale file, no template and nothing under internal/slots, so it stays independent of the Calnode#20 branch.
Author
|
I have read the CLA Document and I hereby sign the CLA |
Contributor
|
All contributors have signed the CLA. ✅ |
distronode-com
force-pushed
the
feat/duplicate-event-type
branch
from
September 4, 2026 08:00
bbee92f to
dbc4b80
Compare
Contributor
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes Initial review of duplicate event type (API + list UI + tests).
- Transactional duplicate endpoint —
POST /v1/event-types/{slug}/duplicatecopies the row viaINSERT … SELECTand every child dataset (questions, hosts, event-scoped availability, reminders) in one tx; bookings and global availability rules stay out. - Safe defaults — copy is inactive, un-archived, under a generated
-copy/-copy-Nslug; price and email template columns are preserved; owner-only like PATCH/DELETE. - Admin list action — ghost icon +
TooltipDuplicate control on owned rows, with in-flight disable and a toast that names the new inactive slug. - Test coverage — child fidelity, cross-user slug uniqueness, authz, rollback,
event_typescolumn drift gate, and explicit price/inactive + all nine email template columns.
Grok | 𝕏
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Fixes #17
Adds
POST /v1/event-types/{slug}/duplicateand a Duplicate action on each row of theevent-types list.
What it copies
One transaction covers the event-type row plus every child dataset:
event_type_questionsoptionsJSON, required, positionevent_type_hostsavailability_rulesevent_type_remindershours_beforeevent_types.msg_*/subj_*Bookings are not copied. A copied booking would be a meeting nobody agreed to, carrying a
live manage link, and it would distort both the per-invitee
max_active_bookingscap andthe double-booking guard.
What is deliberately not inherited
is_activeis forced to 0. A copy exists to be edited; publishing one the moment itis created is the accident this endpoint has to avoid.
archived_atis cleared. Archiving describes the original's lifecycle, and anarchived copy would be invisible in the default list. The copy is inactive anyway, so
nothing becomes bookable by clearing it.
slugis regenerated asintro-call-copy, thenintro-call-copy-2,intro-call-copy-3, … becauseevent_types.slugis UNIQUE.Everything else is copied verbatim,
price_centsandcurrencyincluded. Zeroing acopied price is the more dangerous default: the operator sees a familiar event type,
publishes it, and starts taking free bookings for a paid meeting.
The name is copied verbatim too. Unlike the slug it is not required to be unique, and
it is booker-facing (it appears on the booking page, in confirmation emails and in the
calendar invite), so appending "(copy)" would risk leaking an internal artefact into
customer-facing copy. In the list the copy is distinguishable by its slug and by being
inactive. Happy to switch to
Name (copy)if you would rather have it.Implementation notes
INSERT … SELECT. Every inherited column is named once and itsvalue never passes through the Go process, so it cannot be zeroed or truncated in
transit; the only literals in the statement are the four overridden fields.
so the list is explicit — which means a future migration could silently leave a new
column out of every copy, with no failing test anywhere near it.
TestDuplicateEventType_handlesEveryEventTypeColumnreadspragma_table_info('event_types')and fails unless each column appears either in thecopy or in a documented not-inherited list, and it also fails when a listed column
disappears.
each cursor closed, before anything is written — a write issued while a cursor is open
would wait on the connection that cursor holds and deadlock as
context deadline exceeded. Same shape asloadHostScheduleand the calendar reconciler.event_type_id IS NULLis thehost's global default and already applies to the copy. Copying it would promote a global
rule into an event-specific one, after which the original's later edits would stop
reaching the copy.
so handing them a copy they could not then edit would be worse than a 404.
that consumes its answer are atomic. The check is deliberately not owner-scoped, because
the constraint is not: a slug taken by another user's event type is still taken. After
50 attempts it gives up with a 409 rather than looping.
201with the copy in the same shape asCreateEventType, so the admin UIcan send the operator straight into the new event type's editor if you ever want that.
UI
A ghost icon button plus
Tooltipin the row action group, between Settings and Archive,shown only for event types the viewer owns — the same pattern the archive and delete
actions already use. On success it toasts the generated slug and notes that the copy is
inactive, then reloads the list. The button is disabled while its own request is in
flight, so a double click cannot produce two copies.
Testing
go test ./...green;gofmt -l .empty;go vet ./...clean. Frontendpnpm test:visualpasses (5 tests), as doespnpm check(941 files, 0 errors).New tests in
internal/handler/event_type_duplicate_test.go:copiesTheRowAndEveryChildDataset— seeds a non-default value in every copied columnand one row in each child table, then asserts each dataset arrived, that child rows got
fresh ids rather than being shared, that the global availability rule was left alone,
and that no booking followed.
slugIsUniqueAcrossTheInstance—-copyalready owned by another user, so the firstcopy lands on
-copy-2and the second on-copy-3.archivedSourceProducesALiveDraft— archived source, un-archived and inactive copy.onlyTheOwnerMayDuplicate— an assigned host gets 404 and no copy is created.unknownSlugIs404.rollsBackWhenAChildCopyFails— a trigger forces the question insert to fail; asserts500 and that no event type, host, rule or reminder row survives. A partial copy would be
worse than no copy: it looks like a real event type and is missing exactly the parts
nobody thinks to check.
handlesEveryEventTypeColumn— the drift gate described above.