Limit Coupon Redemptions
Marketing wants coupons that run out. Each coupon now has an expiry and a redemption limit, and each customer can redeem a coupon once. Also adds the HTTP handler for the redeem button.
Mcoupons/service.go1 of 3
Files Changed
coupons/
coupons/service.go
Go · Click a line number to comment · Tap a line to comment@@ -1,20 +1,43 @@
package coupons
import (
"context"
"errors"
Added: "time"
)
Removed: var ErrNotFound = errors.New("coupon not found")
Added: var (
Added: ErrNotFound = errors.New("coupon not found")
Added: ErrExpired = errors.New("coupon expired")
Added: ErrExhausted = errors.New("coupon fully redeemed")
Added: ErrAlreadyRedeemed = errors.New("coupon already redeemed")
Added: )
type Service struct {
store Store
}
func (s *Service) Redeem(ctx context.Context, userID, code string) error {
c, err := s.store.GetCoupon(ctx, code)
if err != nil {
return err
}
Added: if time.Now().After(c.ExpiresAt) {
Added: return ErrExpired
Added: }
Added: used, err := s.store.HasRedeemed(ctx, userID, c.Code)
Added: if err != nil {
Added: return err
Added: }
Added: if used {
Added: return ErrAlreadyRedeemed
Added: }
Added: if c.Redemptions >= c.MaxRedemptions {
Added: return ErrExhausted
Added: }
Added: c.Redemptions++
Added: if err := s.store.SaveCoupon(ctx, c); err != nil {
Added: return err
Added: }
return s.store.AddRedemption(ctx, userID, c.Code)
}
coupons/handler.go
Go · Click a line number to comment · Tap a line to comment@@ -0,0 +1,17 @@
Added: package coupons
Added:
Added: import "net/http"
Added:
Added: type Handler struct {
Added: svc *Service
Added: }
Added:
Added: func (h *Handler) Redeem(w http.ResponseWriter, r *http.Request) {
Added: userID := r.FormValue("user_id")
Added: code := r.FormValue("code")
Added: if err := h.svc.Redeem(r.Context(), userID, code); err != nil {
Added: http.Error(w, err.Error(), http.StatusInternalServerError)
Added: return
Added: }
Added: w.WriteHeader(http.StatusNoContent)
Added: }
coupons/service_test.go
Go · Click a line number to comment · Tap a line to comment@@ -0,0 +1,20 @@
Added: package coupons
Added:
Added: import (
Added: "context"
Added: "errors"
Added: "testing"
Added: "time"
Added: )
Added:
Added: func TestRedeemStopsAtTheLimit(t *testing.T) {
Added: store := newFakeStore(Coupon{Code: "SPRING", MaxRedemptions: 1, ExpiresAt: time.Now().Add(time.Hour)})
Added: svc := &Service{store: store}
Added: if err := svc.Redeem(context.Background(), "u1", "SPRING"); err != nil {
Added: t.Fatalf("first redeem: %v", err)
Added: }
Added: err := svc.Redeem(context.Background(), "u2", "SPRING")
Added: if !errors.Is(err, ErrExhausted) {
Added: t.Fatalf("second redeem: got %v, want ErrExhausted", err)
Added: }
Added: }