package abono import ( "context" "errors" "fmt" "time" "gorm.io/gorm" ) // Comp statuses, as the schema's CHECK allows them. const ( compActive = "active" compCanceled = "canceled" ) type compRow struct { ID int64 Subject string Plan string Status string EndsAt *time.Time Note string CreatedAt time.Time UpdatedAt time.Time } func (compRow) TableName() string { return "abono_comps" } // Comp gives subject the plan without payment, until endsAt or for good // when endsAt is nil, and returns the comp's id. It is a comp, not a // grant, so it is never confused with "grants access". OnChange runs in the // same transaction, and its error undoes the comp. func (s *Service) Comp(ctx context.Context, subject, plan string, endsAt *time.Time, note string) (int64, error) { if subject == "" || plan == "" { return 0, errors.New("abono: Comp needs a subject and a plan") } now := s.now() row := compRow{Subject: subject, Plan: plan, Status: compActive, Note: note, CreatedAt: now, UpdatedAt: now} if endsAt != nil { e := endsAt.UTC() row.EndsAt = &e } err := s.tx(ctx, func(tx *gorm.DB) error { if err := tx.Create(&row).Error; err != nil { return err } return s.changed(ctx, tx, subject) }) if err != nil { return 0, err } return row.ID, nil } // EndComp ends a comp now. Ending one already ended changes nothing and // tells nobody. OnChange runs in the same transaction, and its error leaves // the comp as it was. func (s *Service) EndComp(ctx context.Context, id int64) error { return s.tx(ctx, func(tx *gorm.DB) error { var row compRow switch err := tx.Where("id = ?", id).Take(&row).Error; { case errors.Is(err, gorm.ErrRecordNotFound): return fmt.Errorf("abono: comp %d: %w", id, ErrNotFound) case err != nil: return err } if row.Status == compCanceled { return nil } if err := tx.Exec(`UPDATE abono_comps SET status = ?, updated_at = ? WHERE id = ?`, compCanceled, s.now(), id).Error; err != nil { return err } return s.changed(ctx, tx, row.Subject) }) }