Reapply "feat(loyalty): EnakPoint & EnakCoin" (#32)
This reverts commit 4e24f9bbb0.
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"apskel-pos-be/internal/constants"
|
||||
"apskel-pos-be/internal/contract"
|
||||
"apskel-pos-be/internal/service"
|
||||
"apskel-pos-be/internal/util"
|
||||
)
|
||||
|
||||
// CustomerDeviceHandler serves /customer/devices, where the customer app registers
|
||||
// the FCM token of each device it runs on.
|
||||
type CustomerDeviceHandler struct {
|
||||
devices service.CustomerDeviceService
|
||||
}
|
||||
|
||||
func NewCustomerDeviceHandler(devices service.CustomerDeviceService) *CustomerDeviceHandler {
|
||||
return &CustomerDeviceHandler{devices: devices}
|
||||
}
|
||||
|
||||
// Register is PUT /customer/devices.
|
||||
func (h *CustomerDeviceHandler) Register(c *gin.Context) {
|
||||
customerID, ok := customerIDFromGin(c, "CustomerDeviceHandler::Register")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req contract.RegisterCustomerDeviceRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||
contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, "device_id and fcm_token are required"),
|
||||
}), "CustomerDeviceHandler::Register")
|
||||
return
|
||||
}
|
||||
util.HandleResponse(c.Writer, c.Request, h.devices.Register(c.Request.Context(), customerID, &req), "CustomerDeviceHandler::Register")
|
||||
}
|
||||
|
||||
// Unregister is DELETE /customer/devices/:device_id, for logout.
|
||||
func (h *CustomerDeviceHandler) Unregister(c *gin.Context) {
|
||||
customerID, ok := customerIDFromGin(c, "CustomerDeviceHandler::Unregister")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
util.HandleResponse(c.Writer, c.Request, h.devices.Unregister(c.Request.Context(), customerID, c.Param("device_id")), "CustomerDeviceHandler::Unregister")
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"apskel-pos-be/internal/contract"
|
||||
"apskel-pos-be/internal/service"
|
||||
"apskel-pos-be/internal/util"
|
||||
)
|
||||
|
||||
// CustomerOrderPaymentHandler serves POST /customer/orders/:id/pay-with-points
|
||||
// (docs/prd-point-coin.md F9). The body holds the PIN, so it is never logged.
|
||||
type CustomerOrderPaymentHandler struct {
|
||||
payments service.CustomerOrderPaymentService
|
||||
}
|
||||
|
||||
func NewCustomerOrderPaymentHandler(payments service.CustomerOrderPaymentService) *CustomerOrderPaymentHandler {
|
||||
return &CustomerOrderPaymentHandler{payments: payments}
|
||||
}
|
||||
|
||||
func (h *CustomerOrderPaymentHandler) PayWithPoints(c *gin.Context) {
|
||||
customerID, ok := customerIDFromGin(c, "CustomerOrderPaymentHandler::PayWithPoints")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
orderID, ok := parseUUIDParam(c, "id", "CustomerOrderPaymentHandler::PayWithPoints")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req contract.PayWithPointsRequest
|
||||
if !bindPinRequest(c, &req, "CustomerOrderPaymentHandler::PayWithPoints") {
|
||||
return
|
||||
}
|
||||
util.HandleResponse(c.Writer, c.Request, h.payments.PayWithPoints(c.Request.Context(), customerID, orderID, &req, pinRequestInfo(c)), "CustomerOrderPaymentHandler::PayWithPoints")
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"apskel-pos-be/internal/appcontext"
|
||||
"apskel-pos-be/internal/constants"
|
||||
"apskel-pos-be/internal/contract"
|
||||
"apskel-pos-be/internal/models"
|
||||
"apskel-pos-be/internal/service"
|
||||
"apskel-pos-be/internal/util"
|
||||
)
|
||||
|
||||
// CustomerPinHandler serves /customer/pin and the dashboard's PIN endpoints
|
||||
// (docs/prd-point-coin.md F11). Request bodies hold PINs, so nothing here logs a body,
|
||||
// and binding errors are reported without the values sent.
|
||||
type CustomerPinHandler struct {
|
||||
pinService service.CustomerPinService
|
||||
}
|
||||
|
||||
func NewCustomerPinHandler(pinService service.CustomerPinService) *CustomerPinHandler {
|
||||
return &CustomerPinHandler{pinService: pinService}
|
||||
}
|
||||
|
||||
func (h *CustomerPinHandler) Status(c *gin.Context) {
|
||||
customerID, ok := customerIDFromGin(c, "CustomerPinHandler::Status")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
util.HandleResponse(c.Writer, c.Request, h.pinService.Status(c.Request.Context(), customerID), "CustomerPinHandler::Status")
|
||||
}
|
||||
|
||||
func (h *CustomerPinHandler) RequestOtp(c *gin.Context) {
|
||||
customerID, ok := customerIDFromGin(c, "CustomerPinHandler::RequestOtp")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req contract.RequestPinOtpRequest
|
||||
if !bindPinRequest(c, &req, "CustomerPinHandler::RequestOtp") {
|
||||
return
|
||||
}
|
||||
util.HandleResponse(c.Writer, c.Request, h.pinService.RequestOtp(c.Request.Context(), customerID, &req), "CustomerPinHandler::RequestOtp")
|
||||
}
|
||||
|
||||
func (h *CustomerPinHandler) CreatePin(c *gin.Context) {
|
||||
customerID, ok := customerIDFromGin(c, "CustomerPinHandler::CreatePin")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req contract.CreateCustomerPinRequest
|
||||
if !bindPinRequest(c, &req, "CustomerPinHandler::CreatePin") {
|
||||
return
|
||||
}
|
||||
util.HandleResponse(c.Writer, c.Request, h.pinService.CreatePin(c.Request.Context(), customerID, &req, pinRequestInfo(c)), "CustomerPinHandler::CreatePin")
|
||||
}
|
||||
|
||||
func (h *CustomerPinHandler) ChangePin(c *gin.Context) {
|
||||
customerID, ok := customerIDFromGin(c, "CustomerPinHandler::ChangePin")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req contract.ChangeCustomerPinRequest
|
||||
if !bindPinRequest(c, &req, "CustomerPinHandler::ChangePin") {
|
||||
return
|
||||
}
|
||||
util.HandleResponse(c.Writer, c.Request, h.pinService.ChangePin(c.Request.Context(), customerID, &req, pinRequestInfo(c)), "CustomerPinHandler::ChangePin")
|
||||
}
|
||||
|
||||
func (h *CustomerPinHandler) ResetPin(c *gin.Context) {
|
||||
customerID, ok := customerIDFromGin(c, "CustomerPinHandler::ResetPin")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req contract.ResetCustomerPinRequest
|
||||
if !bindPinRequest(c, &req, "CustomerPinHandler::ResetPin") {
|
||||
return
|
||||
}
|
||||
util.HandleResponse(c.Writer, c.Request, h.pinService.ResetPin(c.Request.Context(), customerID, &req, pinRequestInfo(c)), "CustomerPinHandler::ResetPin")
|
||||
}
|
||||
|
||||
// RemovePin is DELETE /marketing/customers/:id/pin.
|
||||
func (h *CustomerPinHandler) RemovePin(c *gin.Context) {
|
||||
customerID, ok := parseUUIDParam(c, "id", "CustomerPinHandler::RemovePin")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req contract.RemoveCustomerPinRequest
|
||||
if !bindPinRequest(c, &req, "CustomerPinHandler::RemovePin") {
|
||||
return
|
||||
}
|
||||
ctx := c.Request.Context()
|
||||
util.HandleResponse(c.Writer, c.Request, h.pinService.RemovePin(ctx, appcontext.FromGinContext(ctx), customerID, &req, pinRequestInfo(c)), "CustomerPinHandler::RemovePin")
|
||||
}
|
||||
|
||||
// ListSecurityEvents is GET /marketing/customers/:id/security-events.
|
||||
func (h *CustomerPinHandler) ListSecurityEvents(c *gin.Context) {
|
||||
customerID, ok := parseUUIDParam(c, "id", "CustomerPinHandler::ListSecurityEvents")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20"))
|
||||
ctx := c.Request.Context()
|
||||
util.HandleResponse(c.Writer, c.Request, h.pinService.ListSecurityEvents(ctx, appcontext.FromGinContext(ctx), customerID, page, limit), "CustomerPinHandler::ListSecurityEvents")
|
||||
}
|
||||
|
||||
// bindPinRequest binds a JSON body. The error it reports names what is wrong, never the
|
||||
// values, since those can be PINs.
|
||||
func bindPinRequest(c *gin.Context, req interface{}, method string) bool {
|
||||
if err := c.ShouldBindJSON(req); err != nil {
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||
contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, "invalid request body: required fields are missing or have the wrong type"),
|
||||
}), method)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// customerIDFromGin reads the customer set by CustomerAuthMiddleware.
|
||||
func customerIDFromGin(c *gin.Context, method string) (uuid.UUID, bool) {
|
||||
raw, _ := c.Get("customer_id")
|
||||
s, _ := raw.(string)
|
||||
id, err := uuid.Parse(s)
|
||||
if err != nil {
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||
contract.NewResponseError(constants.ValidationErrorCode, constants.AuthHandlerEntity, "Customer ID not found"),
|
||||
}), method)
|
||||
return uuid.Nil, false
|
||||
}
|
||||
return id, true
|
||||
}
|
||||
|
||||
func pinRequestInfo(c *gin.Context) models.CustomerPinRequestInfo {
|
||||
return models.CustomerPinRequestInfo{IPAddress: c.ClientIP(), UserAgent: c.Request.UserAgent()}
|
||||
}
|
||||
|
||||
// IssuePaymentCode is POST /customer/wallet/payment-code.
|
||||
func (h *CustomerPinHandler) IssuePaymentCode(c *gin.Context) {
|
||||
customerID, ok := customerIDFromGin(c, "CustomerPinHandler::IssuePaymentCode")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req contract.IssuePaymentCodeRequest
|
||||
if !bindPinRequest(c, &req, "CustomerPinHandler::IssuePaymentCode") {
|
||||
return
|
||||
}
|
||||
util.HandleResponse(c.Writer, c.Request, h.pinService.IssuePaymentCode(c.Request.Context(), customerID, &req, pinRequestInfo(c)), "CustomerPinHandler::IssuePaymentCode")
|
||||
}
|
||||
@@ -1,9 +1,14 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"apskel-pos-be/internal/constants"
|
||||
"apskel-pos-be/internal/contract"
|
||||
"apskel-pos-be/internal/logger"
|
||||
"apskel-pos-be/internal/models"
|
||||
"apskel-pos-be/internal/processor"
|
||||
"apskel-pos-be/internal/repository"
|
||||
"apskel-pos-be/internal/service"
|
||||
"apskel-pos-be/internal/util"
|
||||
|
||||
@@ -46,7 +51,7 @@ func (h *CustomerPointsHandler) GetCustomerPoints(c *gin.Context) {
|
||||
if err != nil {
|
||||
logger.FromContext(ctx).WithError(err).Error("CustomerPointsHandler::GetCustomerPoints -> service call failed")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||
contract.NewResponseError(constants.InternalServerErrorCode, constants.RequestEntity, err.Error()),
|
||||
contract.NewResponseError(walletErrorCode(err), constants.RequestEntity, err.Error()),
|
||||
}), "CustomerPointsHandler::GetCustomerPoints")
|
||||
return
|
||||
}
|
||||
@@ -80,7 +85,7 @@ func (h *CustomerPointsHandler) GetCustomerTokens(c *gin.Context) {
|
||||
if err != nil {
|
||||
logger.FromContext(ctx).WithError(err).Error("CustomerPointsHandler::GetCustomerTokens -> service call failed")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||
contract.NewResponseError(constants.InternalServerErrorCode, constants.RequestEntity, err.Error()),
|
||||
contract.NewResponseError(walletErrorCode(err), constants.RequestEntity, err.Error()),
|
||||
}), "CustomerPointsHandler::GetCustomerTokens")
|
||||
return
|
||||
}
|
||||
@@ -114,7 +119,7 @@ func (h *CustomerPointsHandler) GetCustomerWallet(c *gin.Context) {
|
||||
if err != nil {
|
||||
logger.FromContext(ctx).WithError(err).Error("CustomerPointsHandler::GetCustomerWallet -> service call failed")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||
contract.NewResponseError(constants.InternalServerErrorCode, constants.RequestEntity, err.Error()),
|
||||
contract.NewResponseError(walletErrorCode(err), constants.RequestEntity, err.Error()),
|
||||
}), "CustomerPointsHandler::GetCustomerWallet")
|
||||
return
|
||||
}
|
||||
@@ -151,3 +156,74 @@ func (h *CustomerPointsHandler) GetFerrisWheelGame(c *gin.Context) {
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "CustomerPointsHandler::GetFerrisWheelGame")
|
||||
}
|
||||
|
||||
// GetCustomerWalletTransactions is GET /customer/wallet/transactions: the ledger, newest
|
||||
// first, filterable by currency, type and date (docs/prd-point-coin.md F6).
|
||||
func (h *CustomerPointsHandler) GetCustomerWalletTransactions(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
customerID, ok := c.Get("customer_id")
|
||||
customerIDStr, isString := customerID.(string)
|
||||
if !ok || !isString {
|
||||
logger.FromContext(ctx).Error("Customer ID not found in context")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||
contract.NewResponseError(constants.ValidationErrorCode, constants.AuthHandlerEntity, "Customer ID not found"),
|
||||
}), "CustomerPointsHandler::GetCustomerWalletTransactions")
|
||||
return
|
||||
}
|
||||
|
||||
var query models.ListCustomerWalletTransactionsQuery
|
||||
if err := c.ShouldBindQuery(&query); err != nil {
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||
contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, err.Error()),
|
||||
}), "CustomerPointsHandler::GetCustomerWalletTransactions")
|
||||
return
|
||||
}
|
||||
|
||||
response, err := h.customerPointsService.GetCustomerWalletTransactions(ctx, customerIDStr, query)
|
||||
if err != nil {
|
||||
logger.FromContext(ctx).WithError(err).Error("CustomerPointsHandler::GetCustomerWalletTransactions -> service call failed")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||
contract.NewResponseError(walletErrorCode(err), constants.RequestEntity, err.Error()),
|
||||
}), "CustomerPointsHandler::GetCustomerWalletTransactions")
|
||||
return
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "CustomerPointsHandler::GetCustomerWalletTransactions")
|
||||
}
|
||||
|
||||
// walletErrorCode tells a bad request apart from a server failure on the wallet
|
||||
// endpoints.
|
||||
func walletErrorCode(err error) string {
|
||||
switch {
|
||||
case errors.Is(err, processor.ErrInvalidWalletQuery):
|
||||
return constants.ValidationErrorCode
|
||||
case errors.Is(err, repository.ErrWalletNotFound):
|
||||
return constants.NotFoundErrorCode
|
||||
default:
|
||||
return constants.InternalServerErrorCode
|
||||
}
|
||||
}
|
||||
|
||||
// GetCustomerWalletExpiring is GET /customer/wallet/expiring: what will expire, per
|
||||
// currency and day (docs/prd-point-coin.md F6).
|
||||
func (h *CustomerPointsHandler) GetCustomerWalletExpiring(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
customerID, ok := c.Get("customer_id")
|
||||
customerIDStr, isString := customerID.(string)
|
||||
if !ok || !isString {
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||
contract.NewResponseError(constants.ValidationErrorCode, constants.AuthHandlerEntity, "Customer ID not found"),
|
||||
}), "CustomerPointsHandler::GetCustomerWalletExpiring")
|
||||
return
|
||||
}
|
||||
response, err := h.customerPointsService.GetCustomerWalletExpiring(ctx, customerIDStr)
|
||||
if err != nil {
|
||||
logger.FromContext(ctx).WithError(err).Error("CustomerPointsHandler::GetCustomerWalletExpiring -> service call failed")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||
contract.NewResponseError(walletErrorCode(err), constants.RequestEntity, err.Error()),
|
||||
}), "CustomerPointsHandler::GetCustomerWalletExpiring")
|
||||
return
|
||||
}
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "CustomerPointsHandler::GetCustomerWalletExpiring")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
|
||||
"apskel-pos-be/internal/constants"
|
||||
applogger "apskel-pos-be/internal/logger"
|
||||
"apskel-pos-be/internal/processor"
|
||||
"apskel-pos-be/internal/repository"
|
||||
"apskel-pos-be/internal/service"
|
||||
)
|
||||
|
||||
// Drives the customer wallet endpoints over HTTP down to Postgres, to show the app
|
||||
// gets the right numbers from the new endpoints and from the old /points, /tokens
|
||||
// and /wallet shapes. Needs TEST_DATABASE_URL pointing at a migrated database; see
|
||||
// internal/repository/wallet_repository_test.go.
|
||||
func TestCustomerWalletEndpoints_AgainstPostgres(t *testing.T) {
|
||||
dsn := os.Getenv("TEST_DATABASE_URL")
|
||||
if dsn == "" {
|
||||
t.Skip("TEST_DATABASE_URL not set")
|
||||
}
|
||||
applogger.Setup("fatal", "json") // the handlers log their errors; the server sets this up at startup
|
||||
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})
|
||||
require.NoError(t, err)
|
||||
|
||||
org, customer, other := uuid.New(), uuid.New(), uuid.New()
|
||||
customers := []uuid.UUID{customer, other}
|
||||
exec := func(q string, args ...any) {
|
||||
t.Helper()
|
||||
require.NoError(t, db.Exec(q, args...).Error)
|
||||
}
|
||||
exec(`INSERT INTO organizations (id, name, plan_type) VALUES (?, 'wallet api test', 'basic')`, org)
|
||||
exec(`INSERT INTO customers (id, organization_id, name) VALUES (?, ?, 'A'), (?, ?, 'B')`, customer, org, other, org)
|
||||
exec(`INSERT INTO organization_settings (organization_id, key, value) VALUES (?, ?, '100')`, org, constants.LoyaltyPointValueKey)
|
||||
t.Cleanup(func() {
|
||||
db.Exec(`DELETE FROM wallet_lot_allocations WHERE lot_id IN (SELECT id FROM wallet_lots WHERE customer_id IN ?)`, customers)
|
||||
db.Exec(`DELETE FROM wallet_lots WHERE customer_id IN ?`, customers)
|
||||
db.Exec(`DELETE FROM wallet_transactions WHERE customer_id IN ?`, customers)
|
||||
db.Exec(`DELETE FROM customer_wallets WHERE customer_id IN ?`, customers)
|
||||
db.Exec(`DELETE FROM customers WHERE id IN ?`, customers)
|
||||
db.Exec(`DELETE FROM organization_settings WHERE organization_id = ?`, org)
|
||||
db.Exec(`DELETE FROM organizations WHERE id = ?`, org)
|
||||
})
|
||||
|
||||
// Balance: 300 earned in two lots that expire on the same Jakarta day but on
|
||||
// different UTC days, 8 coins, 50 paid, and 20 in a lot that has expired but
|
||||
// that the expiry job has not processed.
|
||||
wallet := processor.NewWalletProcessor(repository.NewWalletRepository(db))
|
||||
txm := repository.NewTxManager(db)
|
||||
next := time.Now().Year() + 1
|
||||
lateNightUTC := time.Date(next, 7, 1, 17, 30, 0, 0, time.UTC) // 2 July 00:30 WIB
|
||||
afternoonUTC := time.Date(next, 7, 2, 10, 0, 0, 0, time.UTC) // 2 July 17:00 WIB
|
||||
expired := time.Now().Add(-time.Hour)
|
||||
outlet, order, payment := uuid.New(), uuid.New(), uuid.New()
|
||||
require.NoError(t, txm.WithTransaction(context.Background(), func(ctx context.Context) error {
|
||||
if _, err := wallet.Credit(ctx, processor.WalletCreditInput{
|
||||
WalletEntry: processor.WalletEntry{CustomerID: customer, Currency: constants.WalletCurrencyPoint,
|
||||
Type: constants.WalletTxTypeEarn, Amount: 300, ReferenceType: constants.WalletRefTypeOrder,
|
||||
ReferenceID: order, OutletID: &outlet, Description: "Belanja #ORD-1"},
|
||||
Lots: []processor.WalletLotInput{{Amount: 200, ExpiresAt: &afternoonUTC}, {Amount: 100, ExpiresAt: &lateNightUTC}},
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := wallet.Credit(ctx, processor.WalletCreditInput{WalletEntry: processor.WalletEntry{
|
||||
CustomerID: customer, Currency: constants.WalletCurrencyCoin, Type: constants.WalletTxTypeMigration,
|
||||
Amount: 8, ReferenceType: constants.WalletRefTypeLegacyTokens, ReferenceID: customer,
|
||||
Description: "Saldo awal dari sistem lama"}}); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := wallet.Debit(ctx, processor.WalletDebitInput{WalletEntry: processor.WalletEntry{
|
||||
CustomerID: customer, Currency: constants.WalletCurrencyPoint, Type: constants.WalletTxTypePayment,
|
||||
Amount: 50, ReferenceType: constants.WalletRefTypePayment, ReferenceID: payment, OutletID: &outlet,
|
||||
Description: "Bayar #ORD-2"}}); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := wallet.Credit(ctx, processor.WalletCreditInput{
|
||||
WalletEntry: processor.WalletEntry{CustomerID: customer, Currency: constants.WalletCurrencyPoint,
|
||||
Type: constants.WalletTxTypeEarn, Amount: 20, ReferenceType: constants.WalletRefTypeOrder,
|
||||
ReferenceID: uuid.New(), OutletID: &outlet, Description: "Belanja #ORD-0"},
|
||||
Lots: []processor.WalletLotInput{{Amount: 20, ExpiresAt: &expired}},
|
||||
})
|
||||
return err
|
||||
}))
|
||||
|
||||
h := NewCustomerPointsHandler(service.NewCustomerPointsService(processor.NewCustomerPointsProcessor(
|
||||
processor.NewWalletQueryProcessor(repository.NewWalletQueryRepository(db), processor.NewLoyaltySettingsProcessor(repository.NewLoyaltySettingsRepository(db), repository.NewTxManager(db))),
|
||||
nil,
|
||||
)))
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
as := func(id uuid.UUID) gin.HandlerFunc {
|
||||
return func(c *gin.Context) { c.Set("customer_id", id.String()) }
|
||||
}
|
||||
for _, who := range []struct {
|
||||
prefix string
|
||||
id uuid.UUID
|
||||
}{{"/a", customer}, {"/b", other}, {"/gone", uuid.New()}} {
|
||||
g := router.Group(who.prefix, as(who.id))
|
||||
g.GET("/points", h.GetCustomerPoints)
|
||||
g.GET("/tokens", h.GetCustomerTokens)
|
||||
g.GET("/wallet", h.GetCustomerWallet)
|
||||
g.GET("/wallet/transactions", h.GetCustomerWalletTransactions)
|
||||
}
|
||||
get := func(path string) (int, map[string]any) {
|
||||
t.Helper()
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, path, nil))
|
||||
var body map[string]any
|
||||
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &body), rec.Body.String())
|
||||
return rec.Code, body
|
||||
}
|
||||
// The balance endpoints wrap their data in {status, message, data}.
|
||||
inner := func(body map[string]any) map[string]any {
|
||||
return body["data"].(map[string]any)["data"].(map[string]any)
|
||||
}
|
||||
|
||||
// Old app: /points and /tokens keep their shape and show the spendable balance.
|
||||
status, body := get("/a/points")
|
||||
require.Equal(t, http.StatusOK, status, body)
|
||||
assert.EqualValues(t, 250, inner(body)["total_points"], "300 - 50; the expired 20 is not spendable")
|
||||
assert.Len(t, inner(body)["points_history"], 3)
|
||||
|
||||
status, body = get("/a/tokens")
|
||||
require.Equal(t, http.StatusOK, status, body)
|
||||
assert.EqualValues(t, 8, inner(body)["total_tokens"])
|
||||
|
||||
// /wallet: new fields, with the old ones still there.
|
||||
status, body = get("/a/wallet")
|
||||
require.Equal(t, http.StatusOK, status, body)
|
||||
w := inner(body)
|
||||
assert.EqualValues(t, 250, w["point_balance"])
|
||||
assert.EqualValues(t, 8, w["coin_balance"])
|
||||
assert.EqualValues(t, 100, w["point_value"])
|
||||
assert.EqualValues(t, 25000, w["point_discount_value"])
|
||||
nearest := w["nearest_expiring"].(map[string]any)
|
||||
// The payment took 50 from the lot expiring first, leaving 50 + 200.
|
||||
assert.Equal(t, map[string]any{"amount": float64(250), "date": time.Date(next, 7, 2, 0, 0, 0, 0, time.UTC).Format("2006-01-02")},
|
||||
nearest["point"], "both lots fall on 2 July in Jakarta although they are on different UTC days")
|
||||
assert.Nil(t, nearest["coin"])
|
||||
for _, legacy := range []string{"total_points", "total_tokens", "points_history", "tokens_history", "last_updated"} {
|
||||
assert.Contains(t, w, legacy)
|
||||
}
|
||||
assert.EqualValues(t, 250, w["total_points"])
|
||||
|
||||
recent := w["recent_transactions"].([]any)
|
||||
require.Len(t, recent, 4)
|
||||
for _, raw := range recent {
|
||||
tx := raw.(map[string]any)
|
||||
assert.NotContains(t, tx, "counterparty_customer_id")
|
||||
assert.NotContains(t, tx, "metadata")
|
||||
if tx["amount"].(float64) > 0 {
|
||||
assert.Contains(t, tx, "source", tx["type"])
|
||||
assert.NotContains(t, tx, "destination")
|
||||
} else {
|
||||
assert.Equal(t, map[string]any{"type": "PAYMENT", "id": payment.String()}, tx["destination"])
|
||||
assert.NotContains(t, tx, "source")
|
||||
}
|
||||
}
|
||||
|
||||
// History with filters.
|
||||
status, body = get("/a/wallet/transactions?currency=POINT&type=EARN&limit=1&page=2")
|
||||
require.Equal(t, http.StatusOK, status, body)
|
||||
page := body["data"].(map[string]any)
|
||||
assert.Equal(t, map[string]any{"page": float64(2), "limit": float64(1), "total_count": float64(2), "total_pages": float64(2)}, page["pagination"])
|
||||
rows := page["data"].([]any)
|
||||
require.Len(t, rows, 1)
|
||||
first := rows[0].(map[string]any)
|
||||
assert.Equal(t, "Belanja #ORD-1", first["description"], "newest first, so page 2 is the older earning")
|
||||
assert.Equal(t, map[string]any{"type": "ORDER", "id": order.String()}, first["source"])
|
||||
assert.Equal(t, lateNightUTC.Format(time.RFC3339), parseTime(t, first["expires_at"]).UTC().Format(time.RFC3339))
|
||||
assert.Len(t, first["lots"], 2)
|
||||
|
||||
today := time.Now().In(time.FixedZone("WIB", 7*3600)).Format("2006-01-02")
|
||||
status, body = get("/a/wallet/transactions?from=" + today + "&to=" + today + "&type=PAYMENT,MIGRATION")
|
||||
require.Equal(t, http.StatusOK, status, body)
|
||||
assert.EqualValues(t, 2, body["data"].(map[string]any)["pagination"].(map[string]any)["total_count"])
|
||||
|
||||
status, _ = get("/a/wallet/transactions?currency=GOLD")
|
||||
assert.Equal(t, http.StatusBadRequest, status)
|
||||
status, _ = get("/a/wallet/transactions?limit=abc")
|
||||
assert.Equal(t, http.StatusBadRequest, status)
|
||||
|
||||
// A customer who has never had a wallet sees zeros, not an error.
|
||||
status, body = get("/b/wallet")
|
||||
require.Equal(t, http.StatusOK, status, body)
|
||||
assert.EqualValues(t, 0, inner(body)["point_balance"])
|
||||
assert.Equal(t, []any{}, inner(body)["recent_transactions"])
|
||||
status, body = get("/b/wallet/transactions")
|
||||
require.Equal(t, http.StatusOK, status, body)
|
||||
assert.Equal(t, []any{}, body["data"].(map[string]any)["data"])
|
||||
|
||||
// A token for a customer that no longer exists.
|
||||
status, _ = get("/gone/wallet")
|
||||
assert.Equal(t, http.StatusNotFound, status)
|
||||
}
|
||||
|
||||
func parseTime(t *testing.T, v any) time.Time {
|
||||
t.Helper()
|
||||
s, ok := v.(string)
|
||||
require.True(t, ok, "expected a time string, got %v", v)
|
||||
parsed, err := time.Parse(time.RFC3339Nano, s)
|
||||
require.NoError(t, err)
|
||||
return parsed
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"apskel-pos-be/internal/constants"
|
||||
"apskel-pos-be/internal/contract"
|
||||
"apskel-pos-be/internal/service"
|
||||
"apskel-pos-be/internal/util"
|
||||
)
|
||||
|
||||
// The header exchanges and transfers require (docs/prd-point-coin.md F4, F5), and the
|
||||
// X- form the rest of the API uses, which is accepted too.
|
||||
const (
|
||||
idempotencyKeyHeader = "Idempotency-Key"
|
||||
legacyIdempotencyKeyHeader = "X-Idempotency-Key"
|
||||
)
|
||||
|
||||
// CustomerWalletHandler serves what the customer app does with its own wallet. Bodies
|
||||
// hold the PIN, so they are never logged.
|
||||
type CustomerWalletHandler struct {
|
||||
wallets service.CustomerWalletService
|
||||
}
|
||||
|
||||
func NewCustomerWalletHandler(wallets service.CustomerWalletService) *CustomerWalletHandler {
|
||||
return &CustomerWalletHandler{wallets: wallets}
|
||||
}
|
||||
|
||||
// PreviewExchange is GET /customer/wallet/exchange/preview?coins=.
|
||||
func (h *CustomerWalletHandler) PreviewExchange(c *gin.Context) {
|
||||
customerID, ok := customerIDFromGin(c, "CustomerWalletHandler::PreviewExchange")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var coins int64
|
||||
if raw := c.Query("coins"); raw != "" {
|
||||
var err error
|
||||
coins, err = strconv.ParseInt(raw, 10, 64)
|
||||
if err != nil {
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||
contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "coins must be a whole number"),
|
||||
}), "CustomerWalletHandler::PreviewExchange")
|
||||
return
|
||||
}
|
||||
}
|
||||
util.HandleResponse(c.Writer, c.Request, h.wallets.PreviewExchange(c.Request.Context(), customerID, coins), "CustomerWalletHandler::PreviewExchange")
|
||||
}
|
||||
|
||||
// Exchange is POST /customer/wallet/exchange.
|
||||
func (h *CustomerWalletHandler) Exchange(c *gin.Context) {
|
||||
customerID, ok := customerIDFromGin(c, "CustomerWalletHandler::Exchange")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req contract.ExchangeCoinsRequest
|
||||
if !bindPinRequest(c, &req, "CustomerWalletHandler::Exchange") {
|
||||
return
|
||||
}
|
||||
util.HandleResponse(c.Writer, c.Request, h.wallets.Exchange(c.Request.Context(), customerID, &req, idempotencyKey(c), pinRequestInfo(c)), "CustomerWalletHandler::Exchange")
|
||||
}
|
||||
|
||||
func idempotencyKey(c *gin.Context) string {
|
||||
if key := strings.TrimSpace(c.GetHeader(idempotencyKeyHeader)); key != "" {
|
||||
return key
|
||||
}
|
||||
return strings.TrimSpace(c.GetHeader(legacyIdempotencyKeyHeader))
|
||||
}
|
||||
|
||||
// TransferRecipient is GET /customer/wallet/transfer/recipient?phone=.
|
||||
func (h *CustomerWalletHandler) TransferRecipient(c *gin.Context) {
|
||||
customerID, ok := customerIDFromGin(c, "CustomerWalletHandler::TransferRecipient")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
util.HandleResponse(c.Writer, c.Request, h.wallets.TransferRecipient(c.Request.Context(), customerID, c.Query("phone")), "CustomerWalletHandler::TransferRecipient")
|
||||
}
|
||||
|
||||
// Transfer is POST /customer/wallet/transfer.
|
||||
func (h *CustomerWalletHandler) Transfer(c *gin.Context) {
|
||||
customerID, ok := customerIDFromGin(c, "CustomerWalletHandler::Transfer")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req contract.TransferWalletRequest
|
||||
if !bindPinRequest(c, &req, "CustomerWalletHandler::Transfer") {
|
||||
return
|
||||
}
|
||||
util.HandleResponse(c.Writer, c.Request, h.wallets.Transfer(c.Request.Context(), customerID, &req, idempotencyKey(c), pinRequestInfo(c)), "CustomerWalletHandler::Transfer")
|
||||
}
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"apskel-pos-be/internal/validator"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type GamificationHandler struct {
|
||||
@@ -27,239 +26,6 @@ func NewGamificationHandler(
|
||||
}
|
||||
}
|
||||
|
||||
// Customer Points Handlers
|
||||
func (h *GamificationHandler) CreateCustomerPoints(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
var req contract.CreateCustomerPointsRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::CreateCustomerPoints -> request binding failed")
|
||||
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::CreateCustomerPoints")
|
||||
return
|
||||
}
|
||||
|
||||
validationError, validationErrorCode := h.gamificationValidator.ValidateCreateCustomerPointsRequest(&req)
|
||||
if validationError != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(validationError).Error("GamificationHandler::CreateCustomerPoints -> request validation failed")
|
||||
validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::CreateCustomerPoints")
|
||||
return
|
||||
}
|
||||
|
||||
response, err := h.gamificationService.CreateCustomerPoints(ctx, &req)
|
||||
if err != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::CreateCustomerPoints -> service call failed")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.CustomerPointsEntity, err.Error())}), "GamificationHandler::CreateCustomerPoints")
|
||||
return
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "GamificationHandler::CreateCustomerPoints")
|
||||
}
|
||||
|
||||
func (h *GamificationHandler) GetCustomerPoints(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
idStr := c.Param("id")
|
||||
id, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::GetCustomerPoints -> invalid ID")
|
||||
validationResponseError := contract.NewResponseError(constants.InvalidFieldErrorCode, constants.CustomerPointsEntity, "Invalid ID format")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::GetCustomerPoints")
|
||||
return
|
||||
}
|
||||
|
||||
response, err := h.gamificationService.GetCustomerPoints(ctx, id)
|
||||
if err != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::GetCustomerPoints -> service call failed")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.CustomerPointsEntity, err.Error())}), "GamificationHandler::GetCustomerPoints")
|
||||
return
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "GamificationHandler::GetCustomerPoints")
|
||||
}
|
||||
|
||||
func (h *GamificationHandler) GetCustomerPointsByCustomerID(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
customerIDStr := c.Param("customer_id")
|
||||
customerID, err := uuid.Parse(customerIDStr)
|
||||
if err != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::GetCustomerPointsByCustomerID -> invalid customer ID")
|
||||
validationResponseError := contract.NewResponseError(constants.InvalidFieldErrorCode, constants.CustomerPointsEntity, "Invalid customer ID format")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::GetCustomerPointsByCustomerID")
|
||||
return
|
||||
}
|
||||
|
||||
response, err := h.gamificationService.GetCustomerPointsByCustomerID(ctx, customerID)
|
||||
if err != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::GetCustomerPointsByCustomerID -> service call failed")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.CustomerPointsEntity, err.Error())}), "GamificationHandler::GetCustomerPointsByCustomerID")
|
||||
return
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "GamificationHandler::GetCustomerPointsByCustomerID")
|
||||
}
|
||||
|
||||
func (h *GamificationHandler) ListCustomerPoints(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
var req contract.ListCustomerPointsRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::ListCustomerPoints -> request binding failed")
|
||||
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::ListCustomerPoints")
|
||||
return
|
||||
}
|
||||
|
||||
validationError, validationErrorCode := h.gamificationValidator.ValidateListCustomerPointsRequest(&req)
|
||||
if validationError != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(validationError).Error("GamificationHandler::ListCustomerPoints -> request validation failed")
|
||||
validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::ListCustomerPoints")
|
||||
return
|
||||
}
|
||||
|
||||
response, err := h.gamificationService.ListCustomerPoints(ctx, &req)
|
||||
if err != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::ListCustomerPoints -> service call failed")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.CustomerPointsEntity, err.Error())}), "GamificationHandler::ListCustomerPoints")
|
||||
return
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "GamificationHandler::ListCustomerPoints")
|
||||
}
|
||||
|
||||
func (h *GamificationHandler) UpdateCustomerPoints(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
idStr := c.Param("id")
|
||||
id, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::UpdateCustomerPoints -> invalid ID")
|
||||
validationResponseError := contract.NewResponseError(constants.InvalidFieldErrorCode, constants.CustomerPointsEntity, "Invalid ID format")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::UpdateCustomerPoints")
|
||||
return
|
||||
}
|
||||
|
||||
var req contract.UpdateCustomerPointsRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::UpdateCustomerPoints -> request binding failed")
|
||||
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::UpdateCustomerPoints")
|
||||
return
|
||||
}
|
||||
|
||||
validationError, validationErrorCode := h.gamificationValidator.ValidateUpdateCustomerPointsRequest(&req)
|
||||
if validationError != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(validationError).Error("GamificationHandler::UpdateCustomerPoints -> request validation failed")
|
||||
validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::UpdateCustomerPoints")
|
||||
return
|
||||
}
|
||||
|
||||
response, err := h.gamificationService.UpdateCustomerPoints(ctx, id, &req)
|
||||
if err != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::UpdateCustomerPoints -> service call failed")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.CustomerPointsEntity, err.Error())}), "GamificationHandler::UpdateCustomerPoints")
|
||||
return
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "GamificationHandler::UpdateCustomerPoints")
|
||||
}
|
||||
|
||||
func (h *GamificationHandler) DeleteCustomerPoints(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
idStr := c.Param("id")
|
||||
id, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::DeleteCustomerPoints -> invalid ID")
|
||||
validationResponseError := contract.NewResponseError(constants.InvalidFieldErrorCode, constants.CustomerPointsEntity, "Invalid ID format")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::DeleteCustomerPoints")
|
||||
return
|
||||
}
|
||||
|
||||
err = h.gamificationService.DeleteCustomerPoints(ctx, id)
|
||||
if err != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::DeleteCustomerPoints -> service call failed")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.CustomerPointsEntity, err.Error())}), "GamificationHandler::DeleteCustomerPoints")
|
||||
return
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(nil), "GamificationHandler::DeleteCustomerPoints")
|
||||
}
|
||||
|
||||
func (h *GamificationHandler) AddCustomerPoints(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
customerIDStr := c.Param("customer_id")
|
||||
customerID, err := uuid.Parse(customerIDStr)
|
||||
if err != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::AddCustomerPoints -> invalid customer ID")
|
||||
validationResponseError := contract.NewResponseError(constants.InvalidFieldErrorCode, constants.CustomerPointsEntity, "Invalid customer ID format")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::AddCustomerPoints")
|
||||
return
|
||||
}
|
||||
|
||||
var req contract.AddCustomerPointsRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::AddCustomerPoints -> request binding failed")
|
||||
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::AddCustomerPoints")
|
||||
return
|
||||
}
|
||||
|
||||
validationError, validationErrorCode := h.gamificationValidator.ValidateAddCustomerPointsRequest(&req)
|
||||
if validationError != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(validationError).Error("GamificationHandler::AddCustomerPoints -> request validation failed")
|
||||
validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::AddCustomerPoints")
|
||||
return
|
||||
}
|
||||
|
||||
response, err := h.gamificationService.AddCustomerPoints(ctx, customerID, &req)
|
||||
if err != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::AddCustomerPoints -> service call failed")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.CustomerPointsEntity, err.Error())}), "GamificationHandler::AddCustomerPoints")
|
||||
return
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "GamificationHandler::AddCustomerPoints")
|
||||
}
|
||||
|
||||
func (h *GamificationHandler) DeductCustomerPoints(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
customerIDStr := c.Param("customer_id")
|
||||
customerID, err := uuid.Parse(customerIDStr)
|
||||
if err != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::DeductCustomerPoints -> invalid customer ID")
|
||||
validationResponseError := contract.NewResponseError(constants.InvalidFieldErrorCode, constants.CustomerPointsEntity, "Invalid customer ID format")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::DeductCustomerPoints")
|
||||
return
|
||||
}
|
||||
|
||||
var req contract.DeductCustomerPointsRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::DeductCustomerPoints -> request binding failed")
|
||||
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::DeductCustomerPoints")
|
||||
return
|
||||
}
|
||||
|
||||
validationError, validationErrorCode := h.gamificationValidator.ValidateDeductCustomerPointsRequest(&req)
|
||||
if validationError != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(validationError).Error("GamificationHandler::DeductCustomerPoints -> request validation failed")
|
||||
validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::DeductCustomerPoints")
|
||||
return
|
||||
}
|
||||
|
||||
response, err := h.gamificationService.DeductCustomerPoints(ctx, customerID, &req)
|
||||
if err != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::DeductCustomerPoints -> service call failed")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.CustomerPointsEntity, err.Error())}), "GamificationHandler::DeductCustomerPoints")
|
||||
return
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "GamificationHandler::DeductCustomerPoints")
|
||||
}
|
||||
|
||||
// Play Game Handler
|
||||
func (h *GamificationHandler) PlayGame(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
@@ -288,240 +54,3 @@ func (h *GamificationHandler) PlayGame(c *gin.Context) {
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "GamificationHandler::PlayGame")
|
||||
}
|
||||
|
||||
// Additional handler methods for other gamification features
|
||||
func (h *GamificationHandler) CreateCustomerTokens(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
var req contract.CreateCustomerTokensRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::CreateCustomerTokens -> request binding failed")
|
||||
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::CreateCustomerTokens")
|
||||
return
|
||||
}
|
||||
|
||||
validationError, validationErrorCode := h.gamificationValidator.ValidateCreateCustomerTokensRequest(&req)
|
||||
if validationError != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(validationError).Error("GamificationHandler::CreateCustomerTokens -> request validation failed")
|
||||
validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::CreateCustomerTokens")
|
||||
return
|
||||
}
|
||||
|
||||
response, err := h.gamificationService.CreateCustomerTokens(ctx, &req)
|
||||
if err != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::CreateCustomerTokens -> service call failed")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.CustomerTokensEntity, err.Error())}), "GamificationHandler::CreateCustomerTokens")
|
||||
return
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "GamificationHandler::CreateCustomerTokens")
|
||||
}
|
||||
|
||||
func (h *GamificationHandler) GetCustomerTokens(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
idStr := c.Param("id")
|
||||
id, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::GetCustomerTokens -> invalid ID")
|
||||
validationResponseError := contract.NewResponseError(constants.InvalidFieldErrorCode, constants.CustomerTokensEntity, "Invalid ID format")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::GetCustomerTokens")
|
||||
return
|
||||
}
|
||||
|
||||
response, err := h.gamificationService.GetCustomerTokens(ctx, id)
|
||||
if err != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::GetCustomerTokens -> service call failed")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.CustomerTokensEntity, err.Error())}), "GamificationHandler::GetCustomerTokens")
|
||||
return
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "GamificationHandler::GetCustomerTokens")
|
||||
}
|
||||
|
||||
func (h *GamificationHandler) GetCustomerTokensByCustomerIDAndType(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
customerIDStr := c.Param("customer_id")
|
||||
customerID, err := uuid.Parse(customerIDStr)
|
||||
if err != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::GetCustomerTokensByCustomerIDAndType -> invalid customer ID")
|
||||
validationResponseError := contract.NewResponseError(constants.InvalidFieldErrorCode, constants.CustomerTokensEntity, "Invalid customer ID format")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::GetCustomerTokensByCustomerIDAndType")
|
||||
return
|
||||
}
|
||||
|
||||
tokenType := c.Param("token_type")
|
||||
|
||||
response, err := h.gamificationService.GetCustomerTokensByCustomerIDAndType(ctx, customerID, tokenType)
|
||||
if err != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::GetCustomerTokensByCustomerIDAndType -> service call failed")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.CustomerTokensEntity, err.Error())}), "GamificationHandler::GetCustomerTokensByCustomerIDAndType")
|
||||
return
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "GamificationHandler::GetCustomerTokensByCustomerIDAndType")
|
||||
}
|
||||
|
||||
func (h *GamificationHandler) ListCustomerTokens(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
var req contract.ListCustomerTokensRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::ListCustomerTokens -> request binding failed")
|
||||
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::ListCustomerTokens")
|
||||
return
|
||||
}
|
||||
|
||||
validationError, validationErrorCode := h.gamificationValidator.ValidateListCustomerTokensRequest(&req)
|
||||
if validationError != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(validationError).Error("GamificationHandler::ListCustomerTokens -> request validation failed")
|
||||
validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::ListCustomerTokens")
|
||||
return
|
||||
}
|
||||
|
||||
response, err := h.gamificationService.ListCustomerTokens(ctx, &req)
|
||||
if err != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::ListCustomerTokens -> service call failed")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.CustomerTokensEntity, err.Error())}), "GamificationHandler::ListCustomerTokens")
|
||||
return
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "GamificationHandler::ListCustomerTokens")
|
||||
}
|
||||
|
||||
func (h *GamificationHandler) UpdateCustomerTokens(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
idStr := c.Param("id")
|
||||
id, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::UpdateCustomerTokens -> invalid ID")
|
||||
validationResponseError := contract.NewResponseError(constants.InvalidFieldErrorCode, constants.CustomerTokensEntity, "Invalid ID format")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::UpdateCustomerTokens")
|
||||
return
|
||||
}
|
||||
|
||||
var req contract.UpdateCustomerTokensRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::UpdateCustomerTokens -> request binding failed")
|
||||
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::UpdateCustomerTokens")
|
||||
return
|
||||
}
|
||||
|
||||
validationError, validationErrorCode := h.gamificationValidator.ValidateUpdateCustomerTokensRequest(&req)
|
||||
if validationError != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(validationError).Error("GamificationHandler::UpdateCustomerTokens -> request validation failed")
|
||||
validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::UpdateCustomerTokens")
|
||||
return
|
||||
}
|
||||
|
||||
response, err := h.gamificationService.UpdateCustomerTokens(ctx, id, &req)
|
||||
if err != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::UpdateCustomerTokens -> service call failed")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.CustomerTokensEntity, err.Error())}), "GamificationHandler::UpdateCustomerTokens")
|
||||
return
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "GamificationHandler::UpdateCustomerTokens")
|
||||
}
|
||||
|
||||
func (h *GamificationHandler) DeleteCustomerTokens(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
idStr := c.Param("id")
|
||||
id, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::DeleteCustomerTokens -> invalid ID")
|
||||
validationResponseError := contract.NewResponseError(constants.InvalidFieldErrorCode, constants.CustomerTokensEntity, "Invalid ID format")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::DeleteCustomerTokens")
|
||||
return
|
||||
}
|
||||
|
||||
err = h.gamificationService.DeleteCustomerTokens(ctx, id)
|
||||
if err != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::DeleteCustomerTokens -> service call failed")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.CustomerTokensEntity, err.Error())}), "GamificationHandler::DeleteCustomerTokens")
|
||||
return
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(nil), "GamificationHandler::DeleteCustomerTokens")
|
||||
}
|
||||
|
||||
func (h *GamificationHandler) AddCustomerTokens(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
customerIDStr := c.Param("customer_id")
|
||||
customerID, err := uuid.Parse(customerIDStr)
|
||||
if err != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::AddCustomerTokens -> invalid customer ID")
|
||||
validationResponseError := contract.NewResponseError(constants.InvalidFieldErrorCode, constants.CustomerTokensEntity, "Invalid customer ID format")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::AddCustomerTokens")
|
||||
return
|
||||
}
|
||||
|
||||
tokenType := c.Param("token_type")
|
||||
|
||||
var req contract.AddCustomerTokensRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::AddCustomerTokens -> request binding failed")
|
||||
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::AddCustomerTokens")
|
||||
return
|
||||
}
|
||||
|
||||
validationError, validationErrorCode := h.gamificationValidator.ValidateAddCustomerTokensRequest(&req)
|
||||
if validationError != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(validationError).Error("GamificationHandler::AddCustomerTokens -> request validation failed")
|
||||
validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::AddCustomerTokens")
|
||||
return
|
||||
}
|
||||
|
||||
response, err := h.gamificationService.AddCustomerTokens(ctx, customerID, tokenType, &req)
|
||||
if err != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::AddCustomerTokens -> service call failed")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.CustomerTokensEntity, err.Error())}), "GamificationHandler::AddCustomerTokens")
|
||||
return
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "GamificationHandler::AddCustomerTokens")
|
||||
}
|
||||
|
||||
func (h *GamificationHandler) DeductCustomerTokens(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
customerIDStr := c.Param("customer_id")
|
||||
customerID, err := uuid.Parse(customerIDStr)
|
||||
if err != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::DeductCustomerTokens -> invalid customer ID")
|
||||
validationResponseError := contract.NewResponseError(constants.InvalidFieldErrorCode, constants.CustomerTokensEntity, "Invalid customer ID format")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::DeductCustomerTokens")
|
||||
return
|
||||
}
|
||||
|
||||
tokenType := c.Param("token_type")
|
||||
|
||||
var req contract.DeductCustomerTokensRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::DeductCustomerTokens -> request binding failed")
|
||||
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::DeductCustomerTokens")
|
||||
return
|
||||
}
|
||||
|
||||
validationError, validationErrorCode := h.gamificationValidator.ValidateDeductCustomerTokensRequest(&req)
|
||||
if validationError != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(validationError).Error("GamificationHandler::DeductCustomerTokens -> request validation failed")
|
||||
validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "GamificationHandler::DeductCustomerTokens")
|
||||
return
|
||||
}
|
||||
|
||||
response, err := h.gamificationService.DeductCustomerTokens(ctx, customerID, tokenType, &req)
|
||||
if err != nil {
|
||||
logger.FromContext(c.Request.Context()).WithError(err).Error("GamificationHandler::DeductCustomerTokens -> service call failed")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError(constants.InternalServerErrorCode, constants.CustomerTokensEntity, err.Error())}), "GamificationHandler::DeductCustomerTokens")
|
||||
return
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(response), "GamificationHandler::DeductCustomerTokens")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
|
||||
"apskel-pos-be/internal/appcontext"
|
||||
applogger "apskel-pos-be/internal/logger"
|
||||
"apskel-pos-be/internal/middleware"
|
||||
"apskel-pos-be/internal/processor"
|
||||
"apskel-pos-be/internal/repository"
|
||||
"apskel-pos-be/internal/service"
|
||||
)
|
||||
|
||||
// Drives GET/PUT /outlets/:id/loyalty-settings over HTTP down to Postgres. Needs
|
||||
// TEST_DATABASE_URL pointing at a migrated database; see
|
||||
// internal/repository/wallet_repository_test.go.
|
||||
func TestOutletLoyaltySettingsEndpoints_AgainstPostgres(t *testing.T) {
|
||||
dsn := os.Getenv("TEST_DATABASE_URL")
|
||||
if dsn == "" {
|
||||
t.Skip("TEST_DATABASE_URL not set")
|
||||
}
|
||||
applogger.Setup("fatal", "json")
|
||||
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})
|
||||
require.NoError(t, err)
|
||||
|
||||
org, otherOrg, outlet, admin := uuid.New(), uuid.New(), uuid.New(), uuid.New()
|
||||
exec := func(q string, args ...any) {
|
||||
t.Helper()
|
||||
require.NoError(t, db.Exec(q, args...).Error)
|
||||
}
|
||||
exec(`INSERT INTO organizations (id, name, plan_type) VALUES (?, 'A', 'basic'), (?, 'B', 'basic')`, org, otherOrg)
|
||||
exec(`INSERT INTO outlets (id, organization_id, name) VALUES (?, ?, 'Kemang')`, outlet, org)
|
||||
t.Cleanup(func() {
|
||||
db.Exec(`DELETE FROM loyalty_setting_changes WHERE organization_id IN ?`, []uuid.UUID{org, otherOrg})
|
||||
db.Exec(`DELETE FROM outlet_settings WHERE outlet_id = ?`, outlet)
|
||||
db.Exec(`DELETE FROM organization_settings WHERE organization_id = ?`, org)
|
||||
db.Exec(`DELETE FROM outlets WHERE id = ?`, outlet)
|
||||
db.Exec(`DELETE FROM organizations WHERE id IN ?`, []uuid.UUID{org, otherOrg})
|
||||
})
|
||||
|
||||
settings := processor.NewLoyaltySettingsProcessor(repository.NewLoyaltySettingsRepository(db), repository.NewTxManager(db))
|
||||
h := NewLoyaltySettingsHandler(service.NewLoyaltySettingsService(settings, repository.NewWalletQueryRepository(db)))
|
||||
auth := middleware.NewAuthMiddleware(nil)
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
as := func(orgID uuid.UUID, role string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ctx := context.WithValue(c.Request.Context(), appcontext.OrganizationIDKey, orgID.String())
|
||||
ctx = context.WithValue(ctx, appcontext.UserIDKey, admin.String())
|
||||
ctx = context.WithValue(ctx, appcontext.UserRoleKey, role)
|
||||
c.Request = c.Request.WithContext(ctx)
|
||||
}
|
||||
}
|
||||
for prefix, who := range map[string]struct {
|
||||
org uuid.UUID
|
||||
role string
|
||||
}{"/manager": {org, "manager"}, "/purchasing": {org, "purchasing"}, "/other": {otherOrg, "admin"}} {
|
||||
g := router.Group(prefix, as(who.org, who.role))
|
||||
g.GET("/outlets/:outlet_id/loyalty-settings", h.GetOutletSettings)
|
||||
g.PUT("/outlets/:outlet_id/loyalty-settings", auth.RequireLoyaltyManager(), h.UpdateOutletSettings)
|
||||
}
|
||||
call := func(method, path string, body string) (int, map[string]any) {
|
||||
t.Helper()
|
||||
req := httptest.NewRequest(method, path, bytes.NewBufferString(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
var out map[string]any
|
||||
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &out), rec.Body.String())
|
||||
return rec.Code, out
|
||||
}
|
||||
path := "/outlets/" + outlet.String() + "/loyalty-settings"
|
||||
data := func(body map[string]any) map[string]any { return body["data"].(map[string]any) }
|
||||
|
||||
// An outlet without settings shows every default and 1% cashback.
|
||||
status, body := call(http.MethodGet, "/manager"+path, "")
|
||||
require.Equal(t, http.StatusOK, status, body)
|
||||
got := data(body)
|
||||
assert.Equal(t, map[string]any{"enabled": false, "earn_per_amount": float64(100), "earn_value": float64(1), "min_order_amount": float64(0), "max_per_order": nil}, got["point"])
|
||||
assert.Equal(t, map[string]any{"enabled": false, "earn_per_amount": float64(25000), "earn_value": float64(1), "min_order_amount": float64(0), "max_per_order": nil}, got["coin"])
|
||||
assert.Equal(t, map[string]any{"accept_payment": false, "min_payment_points": float64(1), "max_payment_percent": float64(100)}, got["point_payment"])
|
||||
assert.EqualValues(t, 1, got["point_value"])
|
||||
assert.EqualValues(t, 1, got["point_cashback_percent"])
|
||||
|
||||
// A partial update keeps everything it does not mention.
|
||||
status, body = call(http.MethodPut, "/manager"+path, `{"point": {"enabled": true, "earn_per_amount": 1000}, "coin": {"max_per_order": 3}}`)
|
||||
require.Equal(t, http.StatusOK, status, body)
|
||||
got = data(body)
|
||||
assert.Equal(t, true, got["point"].(map[string]any)["enabled"])
|
||||
assert.EqualValues(t, 1000, got["point"].(map[string]any)["earn_per_amount"])
|
||||
assert.EqualValues(t, 1, got["point"].(map[string]any)["earn_value"], "untouched")
|
||||
assert.EqualValues(t, 25000, got["coin"].(map[string]any)["earn_per_amount"], "untouched")
|
||||
assert.EqualValues(t, 3, got["coin"].(map[string]any)["max_per_order"])
|
||||
assert.EqualValues(t, 0.1, got["point_cashback_percent"], "1 point per Rp 1.000 at Rp 1 a point")
|
||||
assert.Len(t, got["changes"], 3)
|
||||
|
||||
// The cashback follows the organization's point value.
|
||||
orgSettings, err := settings.Organization(context.Background(), org)
|
||||
require.NoError(t, err)
|
||||
orgSettings.PointValue = 5
|
||||
_, _, err = settings.UpdateOrganization(context.Background(), org, admin, *orgSettings)
|
||||
require.NoError(t, err)
|
||||
status, body = call(http.MethodGet, "/manager"+path, "")
|
||||
require.Equal(t, http.StatusOK, status, body)
|
||||
assert.EqualValues(t, 0.5, data(body)["point_cashback_percent"])
|
||||
|
||||
// Sending back what GET returned changes nothing.
|
||||
echo, err := json.Marshal(data(body))
|
||||
require.NoError(t, err)
|
||||
status, body = call(http.MethodPut, "/manager"+path, string(echo))
|
||||
require.Equal(t, http.StatusOK, status, body)
|
||||
assert.Empty(t, data(body)["changes"])
|
||||
|
||||
// null clears a limit.
|
||||
status, body = call(http.MethodPut, "/manager"+path, `{"coin": {"max_per_order": null}}`)
|
||||
require.Equal(t, http.StatusOK, status, body)
|
||||
assert.Nil(t, data(body)["coin"].(map[string]any)["max_per_order"])
|
||||
|
||||
// Values out of bounds, unknown fields and bad JSON are refused and change nothing.
|
||||
for name, bad := range map[string]string{
|
||||
"earn_per_amount 0": `{"point": {"earn_per_amount": 0}}`,
|
||||
"negative earn_value": `{"coin": {"earn_value": -1}}`,
|
||||
"negative min_order": `{"point": {"min_order_amount": -5}}`,
|
||||
"negative max_per_order": `{"point": {"max_per_order": -1}}`,
|
||||
"payment percent over 100": `{"point_payment": {"max_payment_percent": 101}}`,
|
||||
"unknown field": `{"point": {"earn_per_amout": 50}}`,
|
||||
"wrong type": `{"point": {"enabled": "yes"}}`,
|
||||
"not json": `enabled=true`,
|
||||
} {
|
||||
status, _ = call(http.MethodPut, "/manager"+path, bad)
|
||||
assert.Equal(t, http.StatusBadRequest, status, name)
|
||||
}
|
||||
status, body = call(http.MethodGet, "/manager"+path, "")
|
||||
require.Equal(t, http.StatusOK, status, body)
|
||||
assert.EqualValues(t, 1000, data(body)["point"].(map[string]any)["earn_per_amount"])
|
||||
|
||||
// Purchasing staff can read but not change; another organization sees nothing.
|
||||
status, _ = call(http.MethodPut, "/purchasing"+path, `{"point": {"enabled": false}}`)
|
||||
assert.Equal(t, http.StatusForbidden, status)
|
||||
status, _ = call(http.MethodGet, "/other"+path, "")
|
||||
assert.Equal(t, http.StatusNotFound, status)
|
||||
status, _ = call(http.MethodPut, "/other"+path, `{"point": {"enabled": false}}`)
|
||||
assert.Equal(t, http.StatusNotFound, status)
|
||||
status, _ = call(http.MethodGet, "/manager/outlets/not-a-uuid/loyalty-settings", "")
|
||||
assert.Equal(t, http.StatusBadRequest, status)
|
||||
|
||||
// Every change is in the history with who made it.
|
||||
history, err := settings.ListChanges(context.Background(), org, &outlet, 1, 100)
|
||||
require.NoError(t, err)
|
||||
assert.EqualValues(t, 4, history.Pagination.Total, "three keys, then the limit cleared")
|
||||
for _, c := range history.Data {
|
||||
assert.Equal(t, admin, c.ChangedBy)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"io"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"apskel-pos-be/internal/appcontext"
|
||||
"apskel-pos-be/internal/constants"
|
||||
"apskel-pos-be/internal/contract"
|
||||
"apskel-pos-be/internal/logger"
|
||||
"apskel-pos-be/internal/service"
|
||||
"apskel-pos-be/internal/util"
|
||||
)
|
||||
|
||||
// loyaltySettingsBodyLimit caps a settings body; a real one is well under 2 KB.
|
||||
const loyaltySettingsBodyLimit = 64 << 10
|
||||
|
||||
// LoyaltySettingsHandler serves the loyalty settings (docs/prd-point-coin.md F1, F2).
|
||||
type LoyaltySettingsHandler struct {
|
||||
loyaltySettingsService service.LoyaltySettingsService
|
||||
}
|
||||
|
||||
func NewLoyaltySettingsHandler(loyaltySettingsService service.LoyaltySettingsService) *LoyaltySettingsHandler {
|
||||
return &LoyaltySettingsHandler{loyaltySettingsService: loyaltySettingsService}
|
||||
}
|
||||
|
||||
func (h *LoyaltySettingsHandler) GetOutletSettings(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
outletID, ok := parseUUIDParam(c, "outlet_id", "LoyaltySettingsHandler::GetOutletSettings")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
response := h.loyaltySettingsService.GetOutletSettings(ctx, appcontext.FromGinContext(ctx), outletID)
|
||||
if response.HasErrors() {
|
||||
logger.FromContext(ctx).WithError(response.GetErrors()[0]).Error("LoyaltySettingsHandler::GetOutletSettings -> service call failed")
|
||||
}
|
||||
util.HandleResponse(c.Writer, c.Request, response, "LoyaltySettingsHandler::GetOutletSettings")
|
||||
}
|
||||
|
||||
func (h *LoyaltySettingsHandler) UpdateOutletSettings(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
outletID, ok := parseUUIDParam(c, "outlet_id", "LoyaltySettingsHandler::UpdateOutletSettings")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
body, err := io.ReadAll(io.LimitReader(c.Request.Body, loyaltySettingsBodyLimit))
|
||||
if err != nil {
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||
contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "unable to read request body"),
|
||||
}), "LoyaltySettingsHandler::UpdateOutletSettings")
|
||||
return
|
||||
}
|
||||
response := h.loyaltySettingsService.UpdateOutletSettings(ctx, appcontext.FromGinContext(ctx), outletID, body)
|
||||
if response.HasErrors() {
|
||||
logger.FromContext(ctx).WithError(response.GetErrors()[0]).Error("LoyaltySettingsHandler::UpdateOutletSettings -> service call failed")
|
||||
}
|
||||
util.HandleResponse(c.Writer, c.Request, response, "LoyaltySettingsHandler::UpdateOutletSettings")
|
||||
}
|
||||
|
||||
// parseUUIDParam reads a UUID path parameter, answering 400 itself when it is not one.
|
||||
func parseUUIDParam(c *gin.Context, name, method string) (uuid.UUID, bool) {
|
||||
id, err := uuid.Parse(c.Param(name))
|
||||
if err != nil {
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||
contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid "+name),
|
||||
}), method)
|
||||
return uuid.Nil, false
|
||||
}
|
||||
return id, true
|
||||
}
|
||||
|
||||
func (h *LoyaltySettingsHandler) GetOrganizationSettings(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
util.HandleResponse(c.Writer, c.Request, h.loyaltySettingsService.GetOrganizationSettings(ctx, appcontext.FromGinContext(ctx)), "LoyaltySettingsHandler::GetOrganizationSettings")
|
||||
}
|
||||
|
||||
// UpdateOrganizationSettings is PUT /marketing/loyalty-settings. With ?dry_run=true it
|
||||
// saves nothing and returns what would change and what the balances in circulation
|
||||
// would be worth, for the warning the dashboard shows before saving (F2).
|
||||
func (h *LoyaltySettingsHandler) UpdateOrganizationSettings(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
body, err := io.ReadAll(io.LimitReader(c.Request.Body, loyaltySettingsBodyLimit))
|
||||
if err != nil {
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||
contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "unable to read request body"),
|
||||
}), "LoyaltySettingsHandler::UpdateOrganizationSettings")
|
||||
return
|
||||
}
|
||||
response := h.loyaltySettingsService.UpdateOrganizationSettings(ctx, appcontext.FromGinContext(ctx), body, c.Query("dry_run") == "true")
|
||||
if response.HasErrors() {
|
||||
logger.FromContext(ctx).WithError(response.GetErrors()[0]).Error("LoyaltySettingsHandler::UpdateOrganizationSettings -> service call failed")
|
||||
}
|
||||
util.HandleResponse(c.Writer, c.Request, response, "LoyaltySettingsHandler::UpdateOrganizationSettings")
|
||||
}
|
||||
|
||||
// ListHistory is GET /marketing/loyalty-settings/history, optionally for one outlet
|
||||
// (?outlet_id=).
|
||||
func (h *LoyaltySettingsHandler) ListHistory(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
var outletID *uuid.UUID
|
||||
if raw := c.Query("outlet_id"); raw != "" {
|
||||
id, err := uuid.Parse(raw)
|
||||
if err != nil {
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||
contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid outlet_id"),
|
||||
}), "LoyaltySettingsHandler::ListHistory")
|
||||
return
|
||||
}
|
||||
outletID = &id
|
||||
}
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20"))
|
||||
util.HandleResponse(c.Writer, c.Request, h.loyaltySettingsService.ListHistory(ctx, appcontext.FromGinContext(ctx), outletID, page, limit), "LoyaltySettingsHandler::ListHistory")
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
|
||||
"apskel-pos-be/internal/appcontext"
|
||||
applogger "apskel-pos-be/internal/logger"
|
||||
"apskel-pos-be/internal/middleware"
|
||||
"apskel-pos-be/internal/processor"
|
||||
"apskel-pos-be/internal/repository"
|
||||
"apskel-pos-be/internal/service"
|
||||
)
|
||||
|
||||
// Drives GET/PUT /marketing/loyalty-settings and its history over HTTP down to
|
||||
// Postgres. Needs TEST_DATABASE_URL pointing at a migrated database; see
|
||||
// internal/repository/wallet_repository_test.go.
|
||||
func TestOrganizationLoyaltySettingsEndpoints_AgainstPostgres(t *testing.T) {
|
||||
dsn := os.Getenv("TEST_DATABASE_URL")
|
||||
if dsn == "" {
|
||||
t.Skip("TEST_DATABASE_URL not set")
|
||||
}
|
||||
applogger.Setup("fatal", "json")
|
||||
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})
|
||||
require.NoError(t, err)
|
||||
|
||||
org, admin, customer := uuid.New(), uuid.New(), uuid.New()
|
||||
exec := func(q string, args ...any) {
|
||||
t.Helper()
|
||||
require.NoError(t, db.Exec(q, args...).Error)
|
||||
}
|
||||
exec(`INSERT INTO organizations (id, name, plan_type) VALUES (?, 'A', 'basic')`, org)
|
||||
exec(`INSERT INTO customers (id, organization_id, name) VALUES (?, ?, 'Budi')`, customer, org)
|
||||
t.Cleanup(func() {
|
||||
db.Exec(`DELETE FROM wallet_lots WHERE customer_id = ?`, customer)
|
||||
db.Exec(`DELETE FROM wallet_transactions WHERE customer_id = ?`, customer)
|
||||
db.Exec(`DELETE FROM customer_wallets WHERE customer_id = ?`, customer)
|
||||
db.Exec(`DELETE FROM loyalty_setting_changes WHERE organization_id = ?`, org)
|
||||
db.Exec(`DELETE FROM organization_settings WHERE organization_id = ?`, org)
|
||||
db.Exec(`DELETE FROM customers WHERE id = ?`, customer)
|
||||
db.Exec(`DELETE FROM organizations WHERE id = ?`, org)
|
||||
})
|
||||
|
||||
// Balances in circulation: 50.000 EnakPoint and 105 EnakCoin.
|
||||
txm := repository.NewTxManager(db)
|
||||
wallet := processor.NewWalletProcessor(repository.NewWalletRepository(db))
|
||||
for _, c := range []struct {
|
||||
currency, ref string
|
||||
amount int64
|
||||
}{{"POINT", "LEGACY_POINTS", 50000}, {"COIN", "LEGACY_TOKENS", 105}} {
|
||||
require.NoError(t, txm.WithTransaction(context.Background(), func(ctx context.Context) error {
|
||||
_, err := wallet.Credit(ctx, processor.WalletCreditInput{WalletEntry: processor.WalletEntry{
|
||||
CustomerID: customer, Currency: c.currency, Type: "MIGRATION", Amount: c.amount,
|
||||
ReferenceType: c.ref, ReferenceID: uuid.New(), Description: "Saldo awal",
|
||||
Metadata: map[string]interface{}{"frozen": true}}})
|
||||
return err
|
||||
}))
|
||||
}
|
||||
|
||||
settings := processor.NewLoyaltySettingsProcessor(repository.NewLoyaltySettingsRepository(db), txm)
|
||||
h := NewLoyaltySettingsHandler(service.NewLoyaltySettingsService(settings, repository.NewWalletQueryRepository(db)))
|
||||
auth := middleware.NewAuthMiddleware(nil)
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
for prefix, role := range map[string]string{"/manager": "manager", "/purchasing": "purchasing"} {
|
||||
role := role
|
||||
g := router.Group(prefix, func(c *gin.Context) {
|
||||
ctx := context.WithValue(c.Request.Context(), appcontext.OrganizationIDKey, org.String())
|
||||
ctx = context.WithValue(ctx, appcontext.UserIDKey, admin.String())
|
||||
ctx = context.WithValue(ctx, appcontext.UserRoleKey, role)
|
||||
c.Request = c.Request.WithContext(ctx)
|
||||
})
|
||||
g.GET("/loyalty-settings", h.GetOrganizationSettings)
|
||||
g.PUT("/loyalty-settings", auth.RequireLoyaltyManager(), h.UpdateOrganizationSettings)
|
||||
g.GET("/loyalty-settings/history", h.ListHistory)
|
||||
}
|
||||
call := func(method, path, body string) (int, map[string]any) {
|
||||
t.Helper()
|
||||
req := httptest.NewRequest(method, path, bytes.NewBufferString(body))
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
var out map[string]any
|
||||
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &out), rec.Body.String())
|
||||
return rec.Code, out
|
||||
}
|
||||
data := func(body map[string]any) map[string]any { return body["data"].(map[string]any) }
|
||||
|
||||
status, body := call(http.MethodGet, "/manager/loyalty-settings", "")
|
||||
require.Equal(t, http.StatusOK, status, body)
|
||||
got := data(body)
|
||||
assert.EqualValues(t, 1, got["point_value"])
|
||||
assert.Equal(t, map[string]any{"coin_amount": float64(1), "point_amount": float64(1)}, got["exchange"])
|
||||
assert.Equal(t, true, got["transfer"].(map[string]any)["enabled"])
|
||||
assert.Equal(t, "MONTH", got["point_expiry"].(map[string]any)["unit"])
|
||||
impact := got["impact"].(map[string]any)
|
||||
assert.EqualValues(t, 50000, impact["outstanding_points"])
|
||||
assert.EqualValues(t, impact["point_rupiah_before"], impact["point_rupiah_after"])
|
||||
|
||||
// A dry run shows what the balances in circulation would be worth, and saves nothing.
|
||||
change := `{"point_value": 100, "exchange": {"coin_amount": 10, "point_amount": 1}}`
|
||||
status, body = call(http.MethodPut, "/manager/loyalty-settings?dry_run=true", change)
|
||||
require.Equal(t, http.StatusOK, status, body)
|
||||
got = data(body)
|
||||
assert.Equal(t, true, got["dry_run"])
|
||||
impact = got["impact"].(map[string]any)
|
||||
assert.EqualValues(t, 50000, impact["point_rupiah_before"])
|
||||
assert.EqualValues(t, 5000000, impact["point_rupiah_after"])
|
||||
assert.EqualValues(t, 105, impact["coins_as_points_before"])
|
||||
assert.EqualValues(t, 10, impact["coins_as_points_after"])
|
||||
assert.Len(t, got["changes"], 2)
|
||||
status, body = call(http.MethodGet, "/manager/loyalty-settings", "")
|
||||
require.Equal(t, http.StatusOK, status, body)
|
||||
assert.EqualValues(t, 1, data(body)["point_value"], "a dry run saves nothing")
|
||||
status, body = call(http.MethodGet, "/manager/loyalty-settings/history", "")
|
||||
require.Equal(t, http.StatusOK, status, body)
|
||||
assert.EqualValues(t, 0, data(body)["pagination"].(map[string]any)["total_count"])
|
||||
|
||||
// Saving records every change with who made it.
|
||||
status, body = call(http.MethodPut, "/manager/loyalty-settings", change)
|
||||
require.Equal(t, http.StatusOK, status, body)
|
||||
assert.Equal(t, false, data(body)["dry_run"])
|
||||
assert.EqualValues(t, 100, data(body)["point_value"])
|
||||
status, body = call(http.MethodGet, "/manager/loyalty-settings/history", "")
|
||||
require.Equal(t, http.StatusOK, status, body)
|
||||
history := data(body)["data"].([]any)
|
||||
require.Len(t, history, 2)
|
||||
var keys []string
|
||||
for _, raw := range history {
|
||||
row := raw.(map[string]any)
|
||||
keys = append(keys, row["key"].(string))
|
||||
assert.Equal(t, admin.String(), row["changed_by"])
|
||||
assert.Nil(t, row["old_value"], "was on its default")
|
||||
assert.Nil(t, row["outlet_id"])
|
||||
}
|
||||
assert.ElementsMatch(t, []string{"loyalty.point.value", "loyalty.exchange.coin_amount"}, keys)
|
||||
|
||||
// Nothing already written changes: balances and ledger rows keep their numbers.
|
||||
var rows []struct {
|
||||
Amount int64
|
||||
Metadata string
|
||||
}
|
||||
require.NoError(t, db.Raw(`SELECT amount, metadata::text AS metadata FROM wallet_transactions WHERE customer_id = ? ORDER BY currency`, customer).Scan(&rows).Error)
|
||||
require.Len(t, rows, 2)
|
||||
assert.Equal(t, int64(105), rows[0].Amount)
|
||||
assert.Equal(t, int64(50000), rows[1].Amount)
|
||||
for _, r := range rows {
|
||||
assert.Contains(t, r.Metadata, `"frozen": true`)
|
||||
}
|
||||
|
||||
// Out of bounds, unknown fields and the wrong role are refused.
|
||||
for name, bad := range map[string]string{
|
||||
"point value 0": `{"point_value": 0}`,
|
||||
"exchange 0": `{"exchange": {"coin_amount": 0}}`,
|
||||
"expiry unit YEAR": `{"coin_expiry": {"unit": "YEAR"}}`,
|
||||
"unknown field": `{"point_valeu": 5}`,
|
||||
} {
|
||||
status, _ = call(http.MethodPut, "/manager/loyalty-settings", bad)
|
||||
assert.Equal(t, http.StatusBadRequest, status, name)
|
||||
status, _ = call(http.MethodPut, "/manager/loyalty-settings?dry_run=true", bad)
|
||||
assert.Equal(t, http.StatusBadRequest, status, name+" (dry run)")
|
||||
}
|
||||
status, _ = call(http.MethodPut, "/purchasing/loyalty-settings", `{"point_value": 5}`)
|
||||
assert.Equal(t, http.StatusForbidden, status)
|
||||
status, body = call(http.MethodGet, "/manager/loyalty-settings", "")
|
||||
require.Equal(t, http.StatusOK, status, body)
|
||||
assert.EqualValues(t, 100, data(body)["point_value"])
|
||||
}
|
||||
@@ -1,8 +1,11 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"apskel-pos-be/internal/appcontext"
|
||||
"apskel-pos-be/internal/contract"
|
||||
"apskel-pos-be/internal/processor"
|
||||
"apskel-pos-be/internal/service"
|
||||
"apskel-pos-be/internal/transformer"
|
||||
"apskel-pos-be/internal/util"
|
||||
@@ -202,6 +205,11 @@ func (h *OrderHandler) RefundOrder(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := h.orderService.RefundOrder(ctx, id, modelReq, userID); err != nil {
|
||||
// Refusing to hand EnakPoint back as cash is a bad request, not a server fault.
|
||||
if errors.Is(err, processor.ErrPointPaymentRejected) {
|
||||
util.HandleResponse(c.Writer, c.Request, service.PointPaymentErrorResponse(err), "OrderHandler::RefundOrder")
|
||||
return
|
||||
}
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("internal_error", "OrderHandler::RefundOrder", err.Error())}), "OrderHandler::RefundOrder")
|
||||
return
|
||||
}
|
||||
@@ -225,6 +233,11 @@ func (h *OrderHandler) CreatePayment(c *gin.Context) {
|
||||
|
||||
response, err := h.orderService.CreatePayment(c.Request.Context(), modelReq)
|
||||
if err != nil {
|
||||
// A refused EnakPoint payment is the cashier's or customer's to fix, not a server fault.
|
||||
if errors.Is(err, processor.ErrPointPaymentRejected) {
|
||||
util.HandleResponse(c.Writer, c.Request, service.PointPaymentErrorResponse(err), "OrderHandler::CreatePayment")
|
||||
return
|
||||
}
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("internal_error", "OrderHandler::CreatePayment", err.Error())}), "OrderHandler::CreatePayment")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -113,6 +113,16 @@ func (h *PaymentMethodHandler) ListPaymentMethods(c *gin.Context) {
|
||||
|
||||
req.OrganizationID = &contextInfo.OrganizationID
|
||||
|
||||
// At the cashier, EnakPoint is listed only where the outlet accepts it (F9).
|
||||
if outletStr := c.Query("outlet_id"); outletStr != "" {
|
||||
if outletID, err := uuid.Parse(outletStr); err == nil {
|
||||
req.OutletID = &outletID
|
||||
}
|
||||
} else if contextInfo.OutletID != uuid.Nil {
|
||||
outletID := contextInfo.OutletID
|
||||
req.OutletID = &outletID
|
||||
}
|
||||
|
||||
if isActiveStr := c.Query("is_active"); isActiveStr != "" {
|
||||
if isActive, err := strconv.ParseBool(isActiveStr); err == nil {
|
||||
req.IsActive = &isActive
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"apskel-pos-be/internal/appcontext"
|
||||
"apskel-pos-be/internal/service"
|
||||
"apskel-pos-be/internal/util"
|
||||
)
|
||||
|
||||
// PointPaymentHandler serves GET /orders/:id/point-payment/preview
|
||||
// (docs/prd-point-coin.md F9).
|
||||
type PointPaymentHandler struct {
|
||||
pointPaymentService service.PointPaymentService
|
||||
}
|
||||
|
||||
func NewPointPaymentHandler(pointPaymentService service.PointPaymentService) *PointPaymentHandler {
|
||||
return &PointPaymentHandler{pointPaymentService: pointPaymentService}
|
||||
}
|
||||
|
||||
func (h *PointPaymentHandler) Preview(c *gin.Context) {
|
||||
orderID, ok := parseUUIDParam(c, "id", "PointPaymentHandler::Preview")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
ctx := c.Request.Context()
|
||||
util.HandleResponse(c.Writer, c.Request, h.pointPaymentService.Preview(ctx, appcontext.FromGinContext(ctx), orderID), "PointPaymentHandler::Preview")
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"apskel-pos-be/internal/constants"
|
||||
"apskel-pos-be/internal/contract"
|
||||
"apskel-pos-be/internal/mappers"
|
||||
"apskel-pos-be/internal/models"
|
||||
"apskel-pos-be/internal/processor"
|
||||
"apskel-pos-be/internal/service"
|
||||
"apskel-pos-be/internal/util"
|
||||
|
||||
@@ -49,8 +52,14 @@ func (h *SpinGameHandler) PlaySpinGame(c *gin.Context) {
|
||||
// Play the spin game
|
||||
response, err := h.spinGameService.PlaySpinGame(c.Request.Context(), customerID.(string), modelReq)
|
||||
if err != nil {
|
||||
// Not enough EnakCoin, an inactive game or a prize that just ran out is the
|
||||
// customer's to fix, not a server failure.
|
||||
code := constants.InternalServerErrorCode
|
||||
if errors.Is(err, processor.ErrGamePlayRefused) {
|
||||
code = constants.ValidationErrorCode
|
||||
}
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||
contract.NewResponseError(constants.InternalServerErrorCode, constants.SpinGameHandlerEntity, err.Error()),
|
||||
contract.NewResponseError(code, constants.SpinGameHandlerEntity, err.Error()),
|
||||
}), "SpinGameHandler::PlaySpinGame")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
|
||||
"apskel-pos-be/internal/appcontext"
|
||||
"apskel-pos-be/internal/constants"
|
||||
applogger "apskel-pos-be/internal/logger"
|
||||
"apskel-pos-be/internal/processor"
|
||||
"apskel-pos-be/internal/repository"
|
||||
"apskel-pos-be/internal/service"
|
||||
"apskel-pos-be/internal/validator"
|
||||
)
|
||||
|
||||
// Drives the dashboard wallet endpoints over HTTP down to Postgres. Needs
|
||||
// TEST_DATABASE_URL pointing at a migrated database; see
|
||||
// internal/repository/wallet_repository_test.go.
|
||||
func TestWalletAdminEndpoints_AgainstPostgres(t *testing.T) {
|
||||
dsn := os.Getenv("TEST_DATABASE_URL")
|
||||
if dsn == "" {
|
||||
t.Skip("TEST_DATABASE_URL not set")
|
||||
}
|
||||
applogger.Setup("fatal", "json")
|
||||
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})
|
||||
require.NoError(t, err)
|
||||
|
||||
orgA, orgB := uuid.New(), uuid.New()
|
||||
adminA, adminB := uuid.New(), uuid.New()
|
||||
customer := uuid.New()
|
||||
exec := func(q string, args ...any) {
|
||||
t.Helper()
|
||||
require.NoError(t, db.Exec(q, args...).Error)
|
||||
}
|
||||
exec(`INSERT INTO organizations (id, name, plan_type) VALUES (?, 'A', 'basic'), (?, 'B', 'basic')`, orgA, orgB)
|
||||
exec(`INSERT INTO users (id, organization_id, name, email, password_hash, role) VALUES
|
||||
(?, ?, 'Admin Satu', ?, 'x', 'admin'), (?, ?, 'Admin Lain', ?, 'x', 'admin')`,
|
||||
adminA, orgA, adminA.String()+"@test", adminB, orgB, adminB.String()+"@test")
|
||||
exec(`INSERT INTO customers (id, organization_id, name, phone_number) VALUES (?, ?, 'Budi Santoso', '081234567890')`, customer, orgA)
|
||||
t.Cleanup(func() {
|
||||
db.Exec(`DELETE FROM wallet_lot_allocations WHERE lot_id IN (SELECT id FROM wallet_lots WHERE customer_id = ?)`, customer)
|
||||
db.Exec(`DELETE FROM wallet_lots WHERE customer_id = ?`, customer)
|
||||
db.Exec(`DELETE FROM wallet_transactions WHERE customer_id = ?`, customer)
|
||||
db.Exec(`DELETE FROM customer_wallets WHERE customer_id = ?`, customer)
|
||||
db.Exec(`DELETE FROM customers WHERE id = ?`, customer)
|
||||
db.Exec(`DELETE FROM users WHERE id IN ?`, []uuid.UUID{adminA, adminB})
|
||||
db.Exec(`DELETE FROM organizations WHERE id IN ?`, []uuid.UUID{orgA, orgB})
|
||||
})
|
||||
|
||||
walletRepo := repository.NewWalletRepository(db)
|
||||
queryRepo := repository.NewWalletQueryRepository(db)
|
||||
txm := repository.NewTxManager(db)
|
||||
wallet := processor.NewWalletProcessor(walletRepo)
|
||||
require.NoError(t, txm.WithTransaction(context.Background(), func(ctx context.Context) error {
|
||||
outlet := uuid.New()
|
||||
_, err := wallet.Credit(ctx, processor.WalletCreditInput{WalletEntry: processor.WalletEntry{
|
||||
CustomerID: customer, Currency: constants.WalletCurrencyPoint, Type: constants.WalletTxTypeEarn,
|
||||
Amount: 100, ReferenceType: constants.WalletRefTypeOrder, ReferenceID: uuid.New(), OutletID: &outlet,
|
||||
Description: "Belanja #ORD-1"}})
|
||||
return err
|
||||
}))
|
||||
|
||||
h := NewWalletAdminHandler(
|
||||
service.NewWalletAdminService(processor.NewWalletAdminProcessor(repository.NewWalletAdminRepository(db), queryRepo, wallet, processor.NewLoyaltySettingsProcessor(repository.NewLoyaltySettingsRepository(db), txm), txm), processor.NewWalletTraceProcessor(repository.NewWalletTraceRepository(db))),
|
||||
validator.NewWalletValidator(),
|
||||
)
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
as := func(org, user uuid.UUID) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ctx := context.WithValue(c.Request.Context(), appcontext.OrganizationIDKey, org.String())
|
||||
ctx = context.WithValue(ctx, appcontext.UserIDKey, user.String())
|
||||
c.Request = c.Request.WithContext(ctx)
|
||||
}
|
||||
}
|
||||
for prefix, who := range map[string][2]uuid.UUID{"/a": {orgA, adminA}, "/b": {orgB, adminB}} {
|
||||
g := router.Group(prefix, as(who[0], who[1]))
|
||||
g.GET("/customers/:id/wallet", h.GetCustomerWallet)
|
||||
g.POST("/customers/:id/wallet/adjust", h.AdjustCustomerWallet)
|
||||
}
|
||||
call := func(method, path string, body any) (int, map[string]any) {
|
||||
t.Helper()
|
||||
var buf bytes.Buffer
|
||||
if body != nil {
|
||||
require.NoError(t, json.NewEncoder(&buf).Encode(body))
|
||||
}
|
||||
req := httptest.NewRequest(method, path, &buf)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
var out map[string]any
|
||||
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &out), rec.Body.String())
|
||||
return rec.Code, out
|
||||
}
|
||||
adjust := func(prefix string, body map[string]any) (int, map[string]any) {
|
||||
return call(http.MethodPost, prefix+"/customers/"+customer.String()+"/wallet/adjust", body)
|
||||
}
|
||||
spendable := func() float64 {
|
||||
t.Helper()
|
||||
status, body := call(http.MethodGet, "/a/customers/"+customer.String()+"/wallet", nil)
|
||||
require.Equal(t, http.StatusOK, status, body)
|
||||
return body["data"].(map[string]any)["spendable_point_balance"].(float64)
|
||||
}
|
||||
|
||||
// Add 50.
|
||||
status, body := adjust("/a", map[string]any{"currency": "point", "amount": 50, "reason": "komplain #45"})
|
||||
require.Equal(t, http.StatusOK, status, body)
|
||||
tx := body["data"].(map[string]any)["transaction"].(map[string]any)
|
||||
assert.Equal(t, "ADJUSTMENT", tx["type"])
|
||||
assert.EqualValues(t, 50, tx["amount"])
|
||||
assert.Equal(t, "komplain #45", tx["reason"])
|
||||
assert.Equal(t, "Koreksi oleh admin: komplain #45", tx["description"])
|
||||
assert.Equal(t, map[string]any{"id": adminA.String(), "name": "Admin Satu"}, tx["created_by"])
|
||||
assert.Equal(t, map[string]any{"type": "USER", "id": adminA.String()}, tx["source"])
|
||||
assert.EqualValues(t, 150, body["data"].(map[string]any)["spendable_point_balance"])
|
||||
|
||||
// Taking more than the customer can spend is refused and changes nothing.
|
||||
status, body = adjust("/a", map[string]any{"currency": "POINT", "amount": -151, "reason": "salah input"})
|
||||
assert.Equal(t, http.StatusBadRequest, status, body)
|
||||
assert.Equal(t, float64(150), spendable())
|
||||
|
||||
// Taking away, retried with the same key, happens once.
|
||||
for i, wantReplayed := range []bool{false, true} {
|
||||
status, body = adjust("/a", map[string]any{"currency": "POINT", "amount": -30, "reason": "salah input", "idempotency_key": "req-1"})
|
||||
require.Equal(t, http.StatusOK, status, body)
|
||||
assert.Equal(t, wantReplayed, body["data"].(map[string]any)["replayed"], "call %d", i+1)
|
||||
}
|
||||
assert.Equal(t, float64(120), spendable())
|
||||
|
||||
// Bad requests.
|
||||
for name, req := range map[string]map[string]any{
|
||||
"cash-out": {"currency": "POINT", "amount": -10, "reason": "pencairan saldo"},
|
||||
"zero amount": {"currency": "POINT", "amount": 0, "reason": "x"},
|
||||
"no reason": {"currency": "POINT", "amount": 10},
|
||||
"bad currency": {"currency": "GOLD", "amount": 10, "reason": "x"},
|
||||
} {
|
||||
status, _ = adjust("/a", req)
|
||||
assert.Equal(t, http.StatusBadRequest, status, name)
|
||||
}
|
||||
status, _ = call(http.MethodPost, "/a/customers/not-a-uuid/wallet/adjust", map[string]any{"currency": "POINT", "amount": 1, "reason": "x"})
|
||||
assert.Equal(t, http.StatusBadRequest, status)
|
||||
assert.Equal(t, float64(120), spendable())
|
||||
|
||||
// Another organization's admin cannot see or touch this customer.
|
||||
status, _ = call(http.MethodGet, "/b/customers/"+customer.String()+"/wallet", nil)
|
||||
assert.Equal(t, http.StatusNotFound, status)
|
||||
status, _ = adjust("/b", map[string]any{"currency": "POINT", "amount": 1000, "reason": "x"})
|
||||
assert.Equal(t, http.StatusNotFound, status)
|
||||
assert.Equal(t, float64(120), spendable())
|
||||
|
||||
// The dashboard view.
|
||||
status, body = call(http.MethodGet, "/a/customers/"+customer.String()+"/wallet?type=ADJUSTMENT", nil)
|
||||
require.Equal(t, http.StatusOK, status, body)
|
||||
w := body["data"].(map[string]any)
|
||||
assert.Equal(t, map[string]any{"id": customer.String(), "name": "Budi Santoso", "phone": "081234567890"}, w["customer"])
|
||||
assert.EqualValues(t, 120, w["point_balance"])
|
||||
assert.EqualValues(t, 120, w["spendable_point_balance"])
|
||||
lots := w["lots"].([]any)
|
||||
var lotTotal float64
|
||||
for _, l := range lots {
|
||||
lotTotal += l.(map[string]any)["remaining_amount"].(float64)
|
||||
}
|
||||
assert.Equal(t, float64(120), lotTotal)
|
||||
|
||||
rows := w["transactions"].(map[string]any)["data"].([]any)
|
||||
require.Len(t, rows, 2, "the two adjustments, newest first")
|
||||
newest := rows[0].(map[string]any)
|
||||
assert.EqualValues(t, -30, newest["amount"])
|
||||
assert.Equal(t, "salah input", newest["reason"])
|
||||
assert.Equal(t, map[string]any{"id": adminA.String(), "name": "Admin Satu"}, newest["created_by"])
|
||||
assert.Equal(t, map[string]any{"type": "USER", "id": adminA.String()}, newest["destination"])
|
||||
|
||||
// The customer's own history shows the adjustment too, with the reason in the
|
||||
// description and without the admin's identity fields.
|
||||
customerView := NewCustomerPointsHandler(service.NewCustomerPointsService(processor.NewCustomerPointsProcessor(
|
||||
processor.NewWalletQueryProcessor(queryRepo, processor.NewLoyaltySettingsProcessor(repository.NewLoyaltySettingsRepository(db), repository.NewTxManager(db))), nil)))
|
||||
crouter := gin.New()
|
||||
crouter.GET("/wallet/transactions", func(c *gin.Context) { c.Set("customer_id", customer.String()) }, customerView.GetCustomerWalletTransactions)
|
||||
rec := httptest.NewRecorder()
|
||||
crouter.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/wallet/transactions?type=ADJUSTMENT", nil))
|
||||
require.Equal(t, http.StatusOK, rec.Code, rec.Body.String())
|
||||
var cbody map[string]any
|
||||
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &cbody))
|
||||
crows := cbody["data"].(map[string]any)["data"].([]any)
|
||||
require.Len(t, crows, 2)
|
||||
first := crows[0].(map[string]any)
|
||||
assert.Equal(t, "Koreksi oleh admin: salah input", first["description"])
|
||||
assert.NotContains(t, first, "created_by")
|
||||
assert.NotContains(t, first, "reason")
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"apskel-pos-be/internal/appcontext"
|
||||
"apskel-pos-be/internal/constants"
|
||||
"apskel-pos-be/internal/contract"
|
||||
"apskel-pos-be/internal/logger"
|
||||
"apskel-pos-be/internal/models"
|
||||
"apskel-pos-be/internal/service"
|
||||
"apskel-pos-be/internal/util"
|
||||
"apskel-pos-be/internal/validator"
|
||||
)
|
||||
|
||||
// WalletAdminHandler serves /marketing/customers/:id/wallet (docs/prd-point-coin.md F7).
|
||||
type WalletAdminHandler struct {
|
||||
walletAdminService service.WalletAdminService
|
||||
walletValidator validator.WalletValidator
|
||||
}
|
||||
|
||||
func NewWalletAdminHandler(walletAdminService service.WalletAdminService, walletValidator validator.WalletValidator) *WalletAdminHandler {
|
||||
return &WalletAdminHandler{walletAdminService: walletAdminService, walletValidator: walletValidator}
|
||||
}
|
||||
|
||||
func (h *WalletAdminHandler) GetCustomerWallet(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
contextInfo := appcontext.FromGinContext(ctx)
|
||||
|
||||
customerID, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||
contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid customer ID"),
|
||||
}), "WalletAdminHandler::GetCustomerWallet")
|
||||
return
|
||||
}
|
||||
|
||||
var query models.ListCustomerWalletTransactionsQuery
|
||||
if err := c.ShouldBindQuery(&query); err != nil {
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||
contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, err.Error()),
|
||||
}), "WalletAdminHandler::GetCustomerWallet")
|
||||
return
|
||||
}
|
||||
|
||||
response := h.walletAdminService.GetCustomerWallet(ctx, contextInfo, customerID, query)
|
||||
if response.HasErrors() {
|
||||
logger.FromContext(ctx).WithError(response.GetErrors()[0]).Error("WalletAdminHandler::GetCustomerWallet -> service call failed")
|
||||
}
|
||||
util.HandleResponse(c.Writer, c.Request, response, "WalletAdminHandler::GetCustomerWallet")
|
||||
}
|
||||
|
||||
func (h *WalletAdminHandler) AdjustCustomerWallet(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
contextInfo := appcontext.FromGinContext(ctx)
|
||||
|
||||
customerID, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||
contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid customer ID"),
|
||||
}), "WalletAdminHandler::AdjustCustomerWallet")
|
||||
return
|
||||
}
|
||||
|
||||
var req contract.AdjustCustomerWalletRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||
contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()),
|
||||
}), "WalletAdminHandler::AdjustCustomerWallet")
|
||||
return
|
||||
}
|
||||
if validationError, code := h.walletValidator.ValidateAdjustRequest(&req); validationError != nil {
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{
|
||||
contract.NewResponseError(code, constants.RequestEntity, validationError.Error()),
|
||||
}), "WalletAdminHandler::AdjustCustomerWallet")
|
||||
return
|
||||
}
|
||||
|
||||
response := h.walletAdminService.AdjustCustomerWallet(ctx, contextInfo, customerID, &req)
|
||||
if response.HasErrors() {
|
||||
logger.FromContext(ctx).WithError(response.GetErrors()[0]).Error("WalletAdminHandler::AdjustCustomerWallet -> service call failed")
|
||||
}
|
||||
util.HandleResponse(c.Writer, c.Request, response, "WalletAdminHandler::AdjustCustomerWallet")
|
||||
}
|
||||
|
||||
// TraceTransaction is GET /marketing/wallet-transactions/:id/trace.
|
||||
func (h *WalletAdminHandler) TraceTransaction(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
transactionID, ok := parseUUIDParam(c, "id", "WalletAdminHandler::TraceTransaction")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
response := h.walletAdminService.TraceTransaction(ctx, appcontext.FromGinContext(ctx), transactionID)
|
||||
if response.HasErrors() {
|
||||
logger.FromContext(ctx).WithError(response.GetErrors()[0]).Error("WalletAdminHandler::TraceTransaction -> service call failed")
|
||||
}
|
||||
util.HandleResponse(c.Writer, c.Request, response, "WalletAdminHandler::TraceTransaction")
|
||||
}
|
||||
Reference in New Issue
Block a user