1- package admin
1+ package discount
22
33import (
44 "context"
@@ -11,6 +11,7 @@ import (
1111 "github.com/jackc/pgx/v5"
1212 "github.com/sitcon-tw/2026-game/internal/repository"
1313 "github.com/sitcon-tw/2026-game/pkg/helpers"
14+ "github.com/sitcon-tw/2026-game/pkg/middleware"
1415 "github.com/sitcon-tw/2026-game/pkg/res"
1516)
1617
@@ -25,10 +26,10 @@ var (
2526 errScanLookupFailed = errors .New ("scan target lookup failed" )
2627)
2728
28- // AssignCouponByQRCode handles POST /admin/ discount-coupons/scan-assignments.
29- // @Summary 掃描使用者 QR code 發放折扣券
30- // @Description 需要 admin_token cookie。透過使用者的一次性 QR code 發放折扣券,並防止同一 user+discount 重複發放。
31- // @Tags admin
29+ // AssignCouponByQRCode handles POST /discount-coupons/staff /scan-assignments.
30+ // @Summary 掃描使用者 QR code 發放折扣券(工作人員)
31+ // @Description 需要 staff_token cookie。透過使用者的一次性 QR code 發放折扣券,並防止同一 user+discount 重複發放。
32+ // @Tags discount
3233// @Accept json
3334// @Produce json
3435// @Param request body assignCouponByQRCodeRequest true "Assign coupon by QR payload"
3839// @Failure 404 {object} res.ErrorResponse "user not found"
3940// @Failure 409 {object} res.ErrorResponse "already issued by qr scan"
4041// @Failure 500 {object} res.ErrorResponse
41- // @Router /admin/ discount-coupons/scan-assignments [post]
42+ // @Router /discount-coupons/staff /scan-assignments [post]
4243func (h * Handler ) AssignCouponByQRCode (w http.ResponseWriter , r * http.Request ) {
44+ staff , ok := middleware .StaffFromContext (r .Context ())
45+ if ! ok || staff == nil {
46+ res .Fail (w , r , http .StatusUnauthorized , errors .New ("unauthorized" ), "unauthorized staff" )
47+ return
48+ }
49+
4350 var req assignCouponByQRCodeRequest
4451 decoder := json .NewDecoder (r .Body )
4552 decoder .DisallowUnknownFields ()
@@ -72,9 +79,9 @@ func (h *Handler) AssignCouponByQRCode(w http.ResponseWriter, r *http.Request) {
7279 return
7380 }
7481
75- marked , err := h .Repo .TryMarkAdminScanCouponIssued (r .Context (), tx , userID , req .DiscountID )
82+ marked , err := h .Repo .TryMarkStaffScanCouponIssued (r .Context (), tx , userID , req .DiscountID , staff . ID )
7683 if err != nil {
77- res .Fail (w , r , http .StatusInternalServerError , err , "failed to mark admin scan issuance" )
84+ res .Fail (w , r , http .StatusInternalServerError , err , "failed to mark staff scan issuance" )
7885 return
7986 }
8087 if ! marked {
@@ -98,6 +105,64 @@ func (h *Handler) AssignCouponByQRCode(w http.ResponseWriter, r *http.Request) {
98105 _ = json .NewEncoder (w ).Encode (coupon )
99106}
100107
108+ // ListStaffScanHistory handles GET /discount-coupons/staff/current/scan-assignments.
109+ // @Summary 取得工作人員掃碼發券紀錄
110+ // @Description 需要 staff_token cookie,回傳該 staff 透過掃碼發放折扣券的紀錄
111+ // @Tags discount
112+ // @Produce json
113+ // @Success 200 {array} models.StaffQRCouponGrant
114+ // @Failure 401 {object} res.ErrorResponse "unauthorized"
115+ // @Failure 500 {object} res.ErrorResponse
116+ // @Router /discount-coupons/staff/current/scan-assignments [get]
117+ func (h * Handler ) ListStaffScanHistory (w http.ResponseWriter , r * http.Request ) {
118+ staff , ok := middleware .StaffFromContext (r .Context ())
119+ if ! ok || staff == nil {
120+ res .Fail (w , r , http .StatusUnauthorized , errors .New ("unauthorized" ), "unauthorized staff" )
121+ return
122+ }
123+
124+ tx , err := h .Repo .StartTransaction (r .Context ())
125+ if err != nil {
126+ res .Fail (w , r , http .StatusInternalServerError , err , "failed to start transaction" )
127+ return
128+ }
129+ defer h .Repo .DeferRollback (r .Context (), tx )
130+
131+ grants , err := h .Repo .ListStaffScanCouponGrants (r .Context (), tx , staff .ID )
132+ if err != nil {
133+ res .Fail (w , r , http .StatusInternalServerError , err , "failed to list scan history" )
134+ return
135+ }
136+
137+ if err = h .Repo .CommitTransaction (r .Context (), tx ); err != nil {
138+ res .Fail (w , r , http .StatusInternalServerError , err , "failed to commit transaction" )
139+ return
140+ }
141+
142+ type scanGrantItem struct {
143+ UserID string `json:"user_id"`
144+ Nickname string `json:"nickname"`
145+ DiscountID string `json:"discount_id"`
146+ StaffID string `json:"staff_id"`
147+ CreatedAt time.Time `json:"created_at"`
148+ }
149+
150+ resp := make ([]scanGrantItem , 0 , len (grants ))
151+ for _ , g := range grants {
152+ resp = append (resp , scanGrantItem {
153+ UserID : g .UserID ,
154+ Nickname : g .Nickname ,
155+ DiscountID : g .DiscountID ,
156+ StaffID : g .StaffID ,
157+ CreatedAt : g .CreatedAt ,
158+ })
159+ }
160+
161+ w .Header ().Set ("Content-Type" , "application/json; charset=utf-8" )
162+ w .WriteHeader (http .StatusOK )
163+ _ = json .NewEncoder (w ).Encode (resp )
164+ }
165+
101166func (h * Handler ) resolveUserIDFromOneTimeQRCode (ctx context.Context , tx pgx.Tx , token string ) (string , error ) {
102167 userID , err := helpers .VerifyAndExtractUserIDFromOneTimeQRToken (
103168 token ,
0 commit comments