diff --git a/internal/data/events.go b/internal/data/events.go index cf2c62f..7524a3a 100644 --- a/internal/data/events.go +++ b/internal/data/events.go @@ -62,7 +62,9 @@ type EventsQ interface { FilterByStatus(...EventStatus) EventsQ FilterByType(...string) EventsQ FilterByNotType(types ...string) EventsQ - FilterByUpdatedAtBefore(int64) EventsQ FilterByExternalID(string) EventsQ FilterInactiveNotClaimed(types ...string) EventsQ + // FilterByUpdatedAtBefore must be only used with SelectReopenable, because it + // depends on table alias. + FilterByUpdatedAtBefore(int64) EventsQ } diff --git a/internal/data/pg/events.go b/internal/data/pg/events.go index f47642a..2a6f7aa 100644 --- a/internal/data/pg/events.go +++ b/internal/data/pg/events.go @@ -30,7 +30,7 @@ func NewEvents(db *pgdb.DB) data.EventsQ { updater: squirrel.Update(eventsTable), deleter: squirrel.Delete(eventsTable), counter: squirrel.Select("COUNT(*) AS count").From(eventsTable), - reopenable: squirrel.Select("nullifier", "type").Distinct().From(eventsTable + " e1"), + reopenable: squirrel.Select("e1.nullifier", "e1.type").Distinct().From(eventsTable + " e1"), } } @@ -146,7 +146,10 @@ func (q *events) SelectReopenable() ([]data.ReopenableEvent, error) { WHERE e2.nullifier = e1.nullifier AND e2.type = e1.type AND e2.status IN (?, ?))`, eventsTable) - stmt := q.reopenable.Where(subq, data.EventOpen, data.EventFulfilled) + stmt := q.reopenable. + Where(subq, data.EventOpen, data.EventFulfilled). + Join(balancesTable + " b ON b.nullifier = e1.nullifier"). + Where("b.referred_by IS NOT NULL") var res []data.ReopenableEvent if err := q.db.Select(&res, stmt); err != nil { @@ -168,7 +171,7 @@ func (q *events) SelectAbsentTypes(allTypes ...string) ([]data.ReopenableEvent, ) SELECT u.nullifier, t.type FROM ( - SELECT nullifier FROM %s + SELECT nullifier FROM %s WHERE referred_by IS NOT NULL ) u CROSS JOIN types t LEFT JOIN %s e ON e.nullifier = u.nullifier AND e.type = t.type @@ -220,7 +223,8 @@ func (q *events) FilterByExternalID(id string) data.EventsQ { } func (q *events) FilterByUpdatedAtBefore(unix int64) data.EventsQ { - return q.applyCondition(squirrel.Lt{"updated_at": unix}) + q.reopenable = q.reopenable.Where(squirrel.Lt{"e1.updated_at": unix}) + return q } func (q *events) FilterInactiveNotClaimed(types ...string) data.EventsQ { diff --git a/internal/service/handlers/create_event_type.go b/internal/service/handlers/create_event_type.go index 12173e8..ad1cf31 100644 --- a/internal/service/handlers/create_event_type.go +++ b/internal/service/handlers/create_event_type.go @@ -1,6 +1,7 @@ package handlers import ( + "fmt" "net/http" "github.com/rarimo/geo-auth-svc/pkg/auth" @@ -39,23 +40,32 @@ func CreateEventType(w http.ResponseWriter, r *http.Request) { } typeModel := models.ResourceToModel(req.Data.Attributes) - if err = EventTypesQ(r).Insert(typeModel); err != nil { - Log(r).WithError(err).Error("Failed to insert event type") + err = EventsQ(r).Transaction(func() error { + if err = EventTypesQ(r).Insert(typeModel); err != nil { + return fmt.Errorf("insert event type: %w", err) + } + EventTypes(r).Push(typeModel) + + // TODO: add cron jobs for limited events and other special logic when updating other fields is supported + if evtypes.FilterNotOpenable(typeModel) { + return nil + } + return openQREvents(r, typeModel) + }) + + if err != nil { + Log(r).WithError(err).Error("Failed to add event type and open events") ape.RenderErr(w, problems.InternalError()) return } - EventTypes(r).Push(typeModel) - if evtypes.FilterNotOpenable(typeModel) { - w.WriteHeader(http.StatusNoContent) - return - } + w.WriteHeader(http.StatusNoContent) +} +func openQREvents(r *http.Request, evType models.EventType) error { balances, err := BalancesQ(r).FilterDisabled().Select() if err != nil { - Log(r).WithError(err).Error("Failed to select balances") - ape.RenderErr(w, problems.InternalError()) - return + return fmt.Errorf("select balances: %w", err) } eventsToInsert := make([]data.Event, 0, len(balances)) @@ -63,13 +73,13 @@ func CreateEventType(w http.ResponseWriter, r *http.Request) { eventsToInsert = append(eventsToInsert, data.Event{ Nullifier: b.Nullifier, Status: data.EventOpen, - Type: typeModel.Name, + Type: evType.Name, }) } + if err = EventsQ(r).Insert(eventsToInsert...); err != nil { - Log(r).WithError(err).Error("Failed to insert qr-code events") - ape.RenderErr(w, problems.InternalError()) - return + return fmt.Errorf("insert events: %w", err) } - w.WriteHeader(http.StatusNoContent) + + return nil } diff --git a/internal/service/handlers/update_event_type.go b/internal/service/handlers/update_event_type.go index b3c3645..8dc5312 100644 --- a/internal/service/handlers/update_event_type.go +++ b/internal/service/handlers/update_event_type.go @@ -1,9 +1,11 @@ package handlers import ( + "fmt" "net/http" "github.com/rarimo/geo-auth-svc/pkg/auth" + "github.com/rarimo/geo-points-svc/internal/data" "github.com/rarimo/geo-points-svc/internal/data/evtypes/models" "github.com/rarimo/geo-points-svc/internal/service/requests" "github.com/rarimo/geo-points-svc/resources" @@ -33,27 +35,52 @@ func UpdateEventType(w http.ResponseWriter, r *http.Request) { } if evType == nil { Log(r).Debugf("Event type %s not found", req.Data.Attributes.Name) - ape.RenderErr(w, problems.Conflict()) + ape.RenderErr(w, problems.NotFound()) return } } typeModel := models.ResourceToModel(req.Data.Attributes) - res, err := EventTypesQ(r).FilterByNames(typeModel.Name).Update(typeModel.ForUpdate()) + + var updated []models.EventType + err = EventsQ(r).Transaction(func() error { + updated, err = EventTypesQ(r).FilterByNames(typeModel.Name).Update(typeModel.ForUpdate()) + if err != nil { + return fmt.Errorf("update event type: %w", err) + } + if len(updated) != 1 { + return fmt.Errorf("critical: count of updated event types is %d, expected 1", len(updated)) + } + // Currently, event cannot be 'not openable' in other ways, + // add extra checks more fields are supported. + if evType.Disabled == typeModel.Disabled { + return nil + } + // Open events if we have enabled the type, otherwise clean them up. + if !typeModel.Disabled { + return openQREvents(r, typeModel) + } + + deleted, err := EventsQ(r). + FilterByType(typeModel.Name). + FilterByStatus(data.EventOpen, data.EventFulfilled). + Delete() + if err != nil { + return fmt.Errorf("delete disabled events: %w", err) + } + + Log(r).Infof("Deleted %d events on disabling event type %s", deleted, typeModel.Name) + return nil + }) + if err != nil { Log(r).WithError(err).Error("Failed to update event type") ape.RenderErr(w, problems.InternalError()) return } - if len(res) == 0 { - Log(r).Error("Count of updated event_types = 0") - ape.RenderErr(w, problems.InternalError()) - return - } - EventTypes(r).Push(typeModel) - resp := newEventTypeResponse(res[0], r.Header.Get(langHeader)) + resp := newEventTypeResponse(updated[0], r.Header.Get(langHeader)) resp.Data.Attributes.QrCodeValue = typeModel.QRCodeValue ape.Render(w, resp) } diff --git a/internal/service/requests/create_event_type.go b/internal/service/requests/create_event_type.go index 6c1f691..49cba02 100644 --- a/internal/service/requests/create_event_type.go +++ b/internal/service/requests/create_event_type.go @@ -19,6 +19,7 @@ func NewCreateEventType(r *http.Request) (req resources.EventTypeResponse, err e attr := req.Data.Attributes return req, val.Errors{ // only QR code events can be currently created or updated + // localization is not supported currently "data/id": val.Validate(req.Data.ID, val.Required), "data/type": val.Validate(req.Data.Type, val.Required, val.In(resources.EVENT_TYPE)), "data/attributes/action_url": val.Validate(attr.ActionUrl, is.URL), @@ -26,10 +27,15 @@ func NewCreateEventType(r *http.Request) (req resources.EventTypeResponse, err e "data/attributes/frequency": val.Validate(attr.Frequency, val.Required, val.In(string(models.Unlimited))), "data/attributes/logo": val.Validate(attr.Logo, is.URL), "data/attributes/name": val.Validate(attr.Name, val.Required, val.In(req.Data.ID)), - "data/attributes/flag": val.Validate(attr.Flag, val.Empty), - "data/attributes/qr_code_value": val.Validate(attr.QrCodeValue, val.Required), + "data/attributes/qr_code_value": val.Validate(attr.QrCodeValue, val.Required, is.Base64), "data/attributes/reward": val.Validate(attr.Reward, val.Required, val.Min(1)), "data/attributes/short_description": val.Validate(attr.ShortDescription, val.Required), "data/attributes/title": val.Validate(attr.Title, val.Required), + // these fields are not currently supported, because cron jobs implementation is required + "data/attributes/starts_at": val.Validate(attr.StartsAt, val.Empty), + "data/attributes/expires_at": val.Validate(attr.ExpiresAt, val.Empty), + // read-only fields due to reusing the same model + "data/attributes/flag": val.Validate(attr.Flag, val.Empty), + "data/attributes/usage_count": val.Validate(attr.UsageCount, val.Empty), }.Filter() } diff --git a/internal/service/requests/update_event_type.go b/internal/service/requests/update_event_type.go index 66ae6c4..a314b02 100644 --- a/internal/service/requests/update_event_type.go +++ b/internal/service/requests/update_event_type.go @@ -28,5 +28,13 @@ func NewUpdateEventType(r *http.Request) (req resources.EventTypeResponse, err e "data/attributes/frequency": val.Validate(attr.Frequency, val.In(string(models.Unlimited))), "data/attributes/logo": val.Validate(attr.Logo, is.URL), "data/attributes/reward": val.Validate(attr.Reward, val.Min(1)), + // not updatable, as QR code includes event type name + "data/attributes/qr_code_value": val.Validate(attr.QrCodeValue, val.Empty), + // these fields are not currently supported, because cron jobs implementation is required + "data/attributes/starts_at": val.Validate(attr.StartsAt, val.Empty), + "data/attributes/expires_at": val.Validate(attr.ExpiresAt, val.Empty), + // read-only fields due to reusing the same model + "data/attributes/flag": val.Validate(attr.Flag, val.Empty), + "data/attributes/usage_count": val.Validate(attr.UsageCount, val.Empty), }.Filter() }