diff --git a/internal/database/database.go b/internal/database/database.go index d15f53d..80c0a5a 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -160,6 +160,7 @@ func migrate() { amount INTEGER NOT NULL, donor TEXT NOT NULL DEFAULT '', registry_item_id INTEGER REFERENCES registry_items(id), + invitation_id INTEGER REFERENCES invitations(id), confirmed INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL DEFAULT (unixepoch()) ) STRICT`, diff --git a/internal/database/gifts.go b/internal/database/gifts.go index 8e43916..d31450c 100644 --- a/internal/database/gifts.go +++ b/internal/database/gifts.go @@ -2,16 +2,16 @@ package database import "github.com/streambinder/foedus/internal/models" -func CreateGift(amount int, donor string, registryItemID *int) error { +func CreateGift(amount int, donor string, registryItemID, invitationID *int) error { _, err := DB.Exec( - `INSERT INTO gifts (amount, donor, registry_item_id, confirmed) VALUES (?, ?, ?, 0)`, - amount, donor, registryItemID, + `INSERT INTO gifts (amount, donor, registry_item_id, invitation_id, confirmed) VALUES (?, ?, ?, ?, 0)`, + amount, donor, registryItemID, invitationID, ) return err } func GetAllGifts() ([]models.Gift, error) { - rows, err := DB.Query(`SELECT id, amount, donor, registry_item_id, confirmed, created_at FROM gifts ORDER BY confirmed ASC, created_at DESC`) + rows, err := DB.Query(`SELECT id, amount, donor, registry_item_id, invitation_id, confirmed, created_at FROM gifts ORDER BY confirmed ASC, created_at DESC`) if err != nil { return nil, err } @@ -20,7 +20,7 @@ func GetAllGifts() ([]models.Gift, error) { var gifts []models.Gift for rows.Next() { var g models.Gift - if err := rows.Scan(&g.ID, &g.Amount, &g.Donor, &g.RegistryItemID, &g.Confirmed, &g.CreatedAt); err != nil { + if err := rows.Scan(&g.ID, &g.Amount, &g.Donor, &g.RegistryItemID, &g.InvitationID, &g.Confirmed, &g.CreatedAt); err != nil { return nil, err } gifts = append(gifts, g) @@ -31,9 +31,9 @@ func GetAllGifts() ([]models.Gift, error) { func GetGift(id int) (models.Gift, error) { var g models.Gift err := DB.QueryRow( - `SELECT id, amount, donor, registry_item_id, confirmed, created_at FROM gifts WHERE id = ?`, + `SELECT id, amount, donor, registry_item_id, invitation_id, confirmed, created_at FROM gifts WHERE id = ?`, id, - ).Scan(&g.ID, &g.Amount, &g.Donor, &g.RegistryItemID, &g.Confirmed, &g.CreatedAt) + ).Scan(&g.ID, &g.Amount, &g.Donor, &g.RegistryItemID, &g.InvitationID, &g.Confirmed, &g.CreatedAt) return g, err } diff --git a/internal/handlers/gift.go b/internal/handlers/gift.go index 0c7d4c2..5f386a6 100644 --- a/internal/handlers/gift.go +++ b/internal/handlers/gift.go @@ -12,6 +12,7 @@ type claimRequest struct { RegistryItemID *int `json:"registry_item_id"` Amount int `json:"amount"` // whole currency units (e.g. euros) Donor string `json:"donor"` + InviteCode string `json:"invite_code"` // invitation the claimer arrived through, if any } func ClaimGift(c *fiber.Ctx) error { @@ -63,7 +64,8 @@ func ClaimGift(c *fiber.Ctx) error { } } - if err := database.CreateGift(req.Amount, strings.TrimSpace(req.Donor), req.RegistryItemID); err != nil { + invitationID := resolveInvitationID(c, req) + if err := database.CreateGift(req.Amount, strings.TrimSpace(req.Donor), req.RegistryItemID, invitationID); err != nil { logger.Error("gift claim save failed", "amount", req.Amount, "donor", observability.Redact(req.Donor), "error", err.Error()) return c.Status(500).JSON(fiber.Map{"error": "failed to save gift"}) } @@ -72,6 +74,24 @@ func ClaimGift(c *fiber.Ctx) error { "amount", req.Amount, "donor", observability.Redact(req.Donor), "registry_item_id", req.RegistryItemID, + "invitation_id", invitationID, ) return c.JSON(fiber.Map{"ok": true}) } + +// resolveInvitationID resolves the invite code the claimer arrived through to +// its invitation row, so the gift is tied to the invitation with a FK. +// Returns nil when no code was supplied or it doesn't match an invitation — +// the claim is recorded anyway, just without the link. +func resolveInvitationID(c *fiber.Ctx, req claimRequest) *int { + code := strings.TrimSpace(req.InviteCode) + if code == "" { + return nil + } + inv, err := database.GetInvitationByCode(code) + if err != nil { + handlerLogger(c).Warn("gift claim unknown invite code", "invitation_code", observability.Redact(code)) + return nil + } + return &inv.ID +} diff --git a/internal/models/models.go b/internal/models/models.go index c4eead6..5003606 100644 --- a/internal/models/models.go +++ b/internal/models/models.go @@ -157,6 +157,7 @@ type Gift struct { Amount int // whole currency units (e.g. euros), no cents Donor string // who sent the gift RegistryItemID *int // fk to registry_items, nil for generic gifts + InvitationID *int // fk to invitations, nil when the claim wasn't tied to an invitation Confirmed bool CreatedAt Timestamp } diff --git a/templates/dashboard.templ b/templates/dashboard.templ index e8c7229..7b4fbee 100644 --- a/templates/dashboard.templ +++ b/templates/dashboard.templ @@ -151,6 +151,39 @@ func soundtrackInviteGuestFirstNames(invitations []models.Invitation, inviteID s return "" } +// giftsHaveInvitationIDs reports whether any of the given gifts is linked to +// an invitation, so the dashboard can skip the Guests column when unused. +func giftsHaveInvitationIDs(gifts []models.Gift) bool { + for _, g := range gifts { + if g.InvitationID != nil { + return true + } + } + return false +} + +// giftInvitationGuestFirstNames joins the first names of the guests on the +// invitation a gift was claimed through, or "" when the gift has none. +func giftInvitationGuestFirstNames(invitations []models.Invitation, invitationID *int) string { + if invitationID == nil { + return "" + } + for _, inv := range invitations { + if inv.ID != *invitationID { + continue + } + var names []string + for _, guest := range inv.Guests { + firstName := strings.TrimSpace(guest.FirstName) + if firstName != "" { + names = append(names, firstName) + } + } + return strings.Join(names, ", ") + } + return "" +} + func invitationActioned(inv models.Invitation) bool { for _, g := range inv.Guests { if g.ConfirmedCeremony != nil || g.ConfirmedReception != nil { @@ -1293,6 +1326,9 @@ templ Dashboard(settings models.Settings, content SettingsContent, guests []mode