Reapply "feat(loyalty): EnakPoint & EnakCoin" (#32)

This reverts commit 4e24f9bbb0.
This commit is contained in:
efrilm
2026-09-30 15:31:44 +07:00
parent 4e24f9bbb0
commit 582dc75543
195 changed files with 20058 additions and 2177 deletions
+6
View File
@@ -28,7 +28,11 @@ type PaymentMethodAnalyticsResponse struct {
// PaymentMethodSummary represents the summary of payment method analytics
type PaymentMethodSummary struct {
// Money actually received; EnakPoint is reported apart (docs/prd-point-coin.md F9).
TotalAmount float64 `json:"total_amount"`
PointAmount float64 `json:"point_amount"`
PointsUsed int64 `json:"points_used"`
TotalWithPoints float64 `json:"total_with_points"`
TotalOrders int64 `json:"total_orders"`
TotalPayments int64 `json:"total_payments"`
AverageOrderValue float64 `json:"average_order_value"`
@@ -42,6 +46,8 @@ type PaymentMethodAnalyticsData struct {
OrderCount int64 `json:"order_count"`
PaymentCount int64 `json:"payment_count"`
Percentage float64 `json:"percentage"`
PointsUsed int64 `json:"points_used"`
CountsAsCashIn bool `json:"counts_as_cash_in"`
}
type SalesAnalyticsRequest struct {
+5 -5
View File
@@ -12,7 +12,7 @@ import (
type CreateCampaignRequest struct {
Name string `json:"name" binding:"required,min=1,max=150"`
Description *string `json:"description,omitempty"`
Type string `json:"type" binding:"required,oneof=REWARD POINTS TOKENS MIXED"`
Type string `json:"type" binding:"required,oneof=REWARD POINTS COINS TOKENS MIXED"`
StartDate time.Time `json:"start_date" binding:"required"`
EndDate time.Time `json:"end_date" binding:"required"`
IsActive bool `json:"is_active"`
@@ -25,7 +25,7 @@ type UpdateCampaignRequest struct {
ID uuid.UUID `json:"id" binding:"required"`
Name string `json:"name" binding:"required,min=1,max=150"`
Description *string `json:"description,omitempty"`
Type string `json:"type" binding:"required,oneof=REWARD POINTS TOKENS MIXED"`
Type string `json:"type" binding:"required,oneof=REWARD POINTS COINS TOKENS MIXED"`
StartDate time.Time `json:"start_date" binding:"required"`
EndDate time.Time `json:"end_date" binding:"required"`
IsActive bool `json:"is_active"`
@@ -58,7 +58,7 @@ type CreateCampaignRuleRequest struct {
CampaignID uuid.UUID `json:"campaign_id" binding:"required"`
RuleType string `json:"rule_type" binding:"required,oneof=TIER SPEND PRODUCT CATEGORY DAY LOCATION"`
ConditionValue *string `json:"condition_value,omitempty"`
RewardType string `json:"reward_type" binding:"required,oneof=POINTS TOKENS REWARD"`
RewardType string `json:"reward_type" binding:"required,oneof=POINTS COINS TOKENS REWARD"`
RewardValue *int64 `json:"reward_value,omitempty"`
RewardSubtype *string `json:"reward_subtype,omitempty"`
RewardRefID *uuid.UUID `json:"reward_ref_id,omitempty"`
@@ -70,7 +70,7 @@ type UpdateCampaignRuleRequest struct {
CampaignID uuid.UUID `json:"campaign_id" binding:"required"`
RuleType string `json:"rule_type" binding:"required,oneof=TIER SPEND PRODUCT CATEGORY DAY LOCATION"`
ConditionValue *string `json:"condition_value,omitempty"`
RewardType string `json:"reward_type" binding:"required,oneof=POINTS TOKENS REWARD"`
RewardType string `json:"reward_type" binding:"required,oneof=POINTS COINS TOKENS REWARD"`
RewardValue *int64 `json:"reward_value,omitempty"`
RewardSubtype *string `json:"reward_subtype,omitempty"`
RewardRefID *uuid.UUID `json:"reward_ref_id,omitempty"`
@@ -142,7 +142,7 @@ type ListCampaignRulesResponse struct {
type CampaignRuleStruct struct {
RuleType string `json:"rule_type" binding:"required,oneof=TIER SPEND PRODUCT CATEGORY DAY LOCATION"`
ConditionValue *string `json:"condition_value,omitempty"`
RewardType string `json:"reward_type" binding:"required,oneof=POINTS TOKENS REWARD"`
RewardType string `json:"reward_type" binding:"required,oneof=POINTS COINS TOKENS REWARD"`
RewardValue *int64 `json:"reward_value,omitempty"`
RewardSubtype *string `json:"reward_subtype,omitempty"`
RewardRefID *uuid.UUID `json:"reward_ref_id,omitempty"`
@@ -0,0 +1,11 @@
package contract
// RegisterCustomerDeviceRequest is PUT /customer/devices: the FCM token the customer
// app got for this device. Send it after login and whenever FCM refreshes the token.
type RegisterCustomerDeviceRequest struct {
DeviceID string `json:"device_id" binding:"required"`
FCMToken string `json:"fcm_token" binding:"required"`
// android, ios or web.
Platform *string `json:"platform"`
AppVersion *string `json:"app_version"`
}
@@ -0,0 +1,39 @@
package contract
// Requests of /customer/pin and /marketing/customers/:id/pin (docs/prd-point-coin.md
// F11). PINs are strings so a leading zero is kept.
type RequestPinOtpRequest struct {
// pin_setup or pin_reset.
Purpose string `json:"purpose" binding:"required"`
}
type CreateCustomerPinRequest struct {
OtpToken string `json:"otp_token" binding:"required"`
OtpCode string `json:"otp_code" binding:"required"`
Pin string `json:"pin" binding:"required"`
ConfirmPin string `json:"confirm_pin" binding:"required"`
}
type ChangeCustomerPinRequest struct {
OldPin string `json:"old_pin" binding:"required"`
Pin string `json:"pin" binding:"required"`
ConfirmPin string `json:"confirm_pin" binding:"required"`
}
type ResetCustomerPinRequest = CreateCustomerPinRequest
type RemoveCustomerPinRequest struct {
Reason string `json:"reason" binding:"required"`
}
// IssuePaymentCodeRequest is POST /customer/wallet/payment-code.
type IssuePaymentCodeRequest struct {
Pin string `json:"pin" binding:"required"`
}
// PayWithPointsRequest is POST /customer/orders/:id/pay-with-points.
type PayWithPointsRequest struct {
Points int64 `json:"points" binding:"required,min=1"`
Pin string `json:"pin" binding:"required"`
}
@@ -2,66 +2,8 @@ package contract
import (
"time"
"github.com/google/uuid"
)
// Existing gamification contracts
type CreateCustomerPointsRequest struct {
CustomerID uuid.UUID `json:"customer_id" validate:"required"`
Balance int64 `json:"balance" validate:"min=0"`
}
type UpdateCustomerPointsRequest struct {
Balance int64 `json:"balance" validate:"min=0"`
}
type AddCustomerPointsRequest struct {
Points int64 `json:"points" validate:"required,min=1"`
}
type DeductCustomerPointsRequest struct {
Points int64 `json:"points" validate:"required,min=1"`
}
type CustomerPointsResponse struct {
ID uuid.UUID `json:"id"`
CustomerID uuid.UUID `json:"customer_id"`
Balance int64 `json:"balance"`
Customer *CustomerResponse `json:"customer,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type ListCustomerPointsRequest struct {
Page int `json:"page" validate:"min=1"`
Limit int `json:"limit" validate:"min=1,max=100"`
Search string `json:"search"`
SortBy string `json:"sort_by" validate:"omitempty,oneof=balance created_at updated_at"`
SortOrder string `json:"sort_order" validate:"omitempty,oneof=asc desc"`
}
type PaginatedCustomerPointsResponse struct {
Data []CustomerPointsResponse `json:"data"`
TotalCount int `json:"total_count"`
Page int `json:"page"`
Limit int `json:"limit"`
TotalPages int `json:"total_pages"`
}
// New customer API contracts
type GetCustomerPointsRequest struct {
// No additional fields needed - customer ID comes from JWT token
}
type GetCustomerTokensRequest struct {
// No additional fields needed - customer ID comes from JWT token
}
type GetCustomerWalletRequest struct {
// No additional fields needed - customer ID comes from JWT token
}
// Response Contracts
type GetCustomerPointsResponse struct {
Status string `json:"status"`
@@ -1,52 +0,0 @@
package contract
import (
"time"
"github.com/google/uuid"
)
type CreateCustomerTokensRequest struct {
CustomerID uuid.UUID `json:"customer_id" validate:"required"`
TokenType string `json:"token_type" validate:"required,oneof=SPIN RAFFLE MINIGAME"`
Balance int64 `json:"balance" validate:"min=0"`
}
type UpdateCustomerTokensRequest struct {
Balance int64 `json:"balance" validate:"min=0"`
}
type AddCustomerTokensRequest struct {
Tokens int64 `json:"tokens" validate:"required,min=1"`
}
type DeductCustomerTokensRequest struct {
Tokens int64 `json:"tokens" validate:"required,min=1"`
}
type CustomerTokensResponse struct {
ID uuid.UUID `json:"id"`
CustomerID uuid.UUID `json:"customer_id"`
TokenType string `json:"token_type"`
Balance int64 `json:"balance"`
Customer *CustomerResponse `json:"customer,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type ListCustomerTokensRequest struct {
Page int `json:"page" validate:"min=1"`
Limit int `json:"limit" validate:"min=1,max=100"`
Search string `json:"search"`
TokenType string `json:"token_type" validate:"omitempty,oneof=SPIN RAFFLE MINIGAME"`
SortBy string `json:"sort_by" validate:"omitempty,oneof=balance token_type created_at updated_at"`
SortOrder string `json:"sort_order" validate:"omitempty,oneof=asc desc"`
}
type PaginatedCustomerTokensResponse struct {
Data []CustomerTokensResponse `json:"data"`
TotalCount int `json:"total_count"`
Page int `json:"page"`
Limit int `json:"limit"`
TotalPages int `json:"total_pages"`
}
+15 -10
View File
@@ -9,15 +9,17 @@ import (
type CreateGamePlayRequest struct {
GameID uuid.UUID `json:"game_id" validate:"required"`
CustomerID uuid.UUID `json:"customer_id" validate:"required"`
TokenUsed int `json:"token_used" validate:"min=0"`
CoinsUsed int `json:"coins_used" validate:"min=0"`
RandomSeed *string `json:"random_seed,omitempty"`
}
type GamePlayResponse struct {
ID uuid.UUID `json:"id"`
GameID uuid.UUID `json:"game_id"`
CustomerID uuid.UUID `json:"customer_id"`
PrizeID *uuid.UUID `json:"prize_id,omitempty"`
ID uuid.UUID `json:"id"`
GameID uuid.UUID `json:"game_id"`
CustomerID uuid.UUID `json:"customer_id"`
PrizeID *uuid.UUID `json:"prize_id,omitempty"`
CoinsUsed int `json:"coins_used"`
// Deprecated: the same as CoinsUsed, kept until the apps read coins_used.
TokenUsed int `json:"token_used"`
RandomSeed *string `json:"random_seed,omitempty"`
CreatedAt time.Time `json:"created_at"`
@@ -33,7 +35,7 @@ type ListGamePlaysRequest struct {
GameID *uuid.UUID `json:"game_id"`
CustomerID *uuid.UUID `json:"customer_id"`
PrizeID *uuid.UUID `json:"prize_id"`
SortBy string `json:"sort_by" validate:"omitempty,oneof=created_at token_used"`
SortBy string `json:"sort_by" validate:"omitempty,oneof=created_at coins_used token_used"`
SortOrder string `json:"sort_order" validate:"omitempty,oneof=asc desc"`
}
@@ -45,14 +47,17 @@ type PaginatedGamePlaysResponse struct {
TotalPages int `json:"total_pages"`
}
// PlayGameRequest plays a game for a customer. What it costs is the game's
// metadata.coin_cost (docs/prd-point-coin.md F8), never the caller's choice.
type PlayGameRequest struct {
GameID uuid.UUID `json:"game_id" validate:"required"`
CustomerID uuid.UUID `json:"customer_id" validate:"required"`
TokenUsed int `json:"token_used" validate:"min=0"`
}
type PlayGameResponse struct {
GamePlay GamePlayResponse `json:"game_play"`
PrizeWon *GamePrizeResponse `json:"prize_won,omitempty"`
TokensRemaining int64 `json:"tokens_remaining"`
GamePlay GamePlayResponse `json:"game_play"`
PrizeWon *GamePrizeResponse `json:"prize_won,omitempty"`
CoinsRemaining int64 `json:"coins_remaining"`
// Deprecated: the same as CoinsRemaining, kept until the apps read coins_remaining.
TokensRemaining int64 `json:"tokens_remaining"`
}
+28 -16
View File
@@ -92,6 +92,10 @@ type OrderResponse struct {
TotalPaid float64 `json:"total_paid"`
PaymentCount int `json:"payment_count"`
SplitType *string `json:"split_type,omitempty"`
// EnakPoint and EnakCoin the order earned, for the order screen and the receipt
// (docs/prd-point-coin.md F3). Zero when it earned nothing.
PointsEarned int64 `json:"points_earned"`
CoinsEarned int64 `json:"coins_earned"`
}
type OrderItemResponse struct {
@@ -182,9 +186,13 @@ type SetOrderCustomerResponse struct {
}
type CreatePaymentRequest struct {
OrderID uuid.UUID `json:"order_id" validate:"required"`
PaymentMethodID uuid.UUID `json:"payment_method_id" validate:"required"`
Amount float64 `json:"amount" validate:"required,min=0"`
OrderID uuid.UUID `json:"order_id" validate:"required"`
PaymentMethodID uuid.UUID `json:"payment_method_id" validate:"required"`
// For the EnakPoint method: how many to use and the code the customer shows. The
// amount is then computed from them.
Points *int64 `json:"points,omitempty"`
PaymentCode *string `json:"payment_code,omitempty"`
Amount float64 `json:"amount" validate:"min=0"`
TransactionID *string `json:"transaction_id,omitempty" validate:"omitempty"`
SplitNumber int `json:"split_number,omitempty" validate:"omitempty,min=1"`
SplitTotal int `json:"split_total,omitempty" validate:"omitempty,min=1"`
@@ -200,19 +208,23 @@ type CreatePaymentOrderItemRequest struct {
}
type PaymentResponse struct {
ID uuid.UUID `json:"id"`
OrderID uuid.UUID `json:"order_id"`
PaymentMethodID uuid.UUID `json:"payment_method_id"`
PaymentMethodName string `json:"payment_method_name"`
PaymentMethodType string `json:"payment_method_type"`
Amount float64 `json:"amount"`
Status string `json:"status"`
TransactionID *string `json:"transaction_id,omitempty"`
SplitNumber int `json:"split_number"`
SplitTotal int `json:"split_total"`
SplitType *string `json:"split_type,omitempty"`
SplitDescription *string `json:"split_description,omitempty"`
RefundAmount float64 `json:"refund_amount"`
ID uuid.UUID `json:"id"`
OrderID uuid.UUID `json:"order_id"`
PaymentMethodID uuid.UUID `json:"payment_method_id"`
PaymentMethodName string `json:"payment_method_name"`
PaymentMethodType string `json:"payment_method_type"`
Amount float64 `json:"amount"`
Status string `json:"status"`
TransactionID *string `json:"transaction_id,omitempty"`
SplitNumber int `json:"split_number"`
SplitTotal int `json:"split_total"`
SplitType *string `json:"split_type,omitempty"`
SplitDescription *string `json:"split_description,omitempty"`
RefundAmount float64 `json:"refund_amount"`
// Set for a payment with EnakPoint, for "EnakPoint: 50.000 (Rp 50.000)" on the
// receipt (docs/prd-point-coin.md F9).
PointsUsed *int64 `json:"points_used,omitempty"`
PointValue *float64 `json:"point_value,omitempty"`
RefundReason *string `json:"refund_reason,omitempty"`
RefundedAt *time.Time `json:"refunded_at,omitempty"`
RefundedBy *uuid.UUID `json:"refunded_by,omitempty"`
+8 -6
View File
@@ -18,7 +18,7 @@ type CreatePaymentMethodRequest struct {
type UpdatePaymentMethodRequest struct {
Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=100"`
Type *string `json:"type,omitempty" validate:"omitempty,oneof=cash card digital_wallet qr edc"`
Type *string `json:"type,omitempty" validate:"omitempty,oneof=cash card digital_wallet qr edc point"`
Processor *string `json:"processor,omitempty" validate:"omitempty,max=100"`
Configuration map[string]interface{} `json:"configuration,omitempty"`
IsActive *bool `json:"is_active,omitempty"`
@@ -38,11 +38,13 @@ type PaymentMethodResponse struct {
type ListPaymentMethodsRequest struct {
OrganizationID *uuid.UUID `json:"organization_id,omitempty"`
Type *string `json:"type,omitempty" validate:"omitempty,oneof=cash card digital_wallet qr edc"`
IsActive *bool `json:"is_active,omitempty"`
Search string `json:"search,omitempty"`
Page int `json:"page" validate:"min=1"`
Limit int `json:"limit" validate:"min=1,max=100"`
// When set, EnakPoint is left out unless the outlet accepts it (F9).
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
Type *string `json:"type,omitempty" validate:"omitempty,oneof=cash card digital_wallet qr edc point"`
IsActive *bool `json:"is_active,omitempty"`
Search string `json:"search,omitempty"`
Page int `json:"page" validate:"min=1"`
Limit int `json:"limit" validate:"min=1,max=100"`
}
type ListPaymentMethodsResponse struct {
+5 -3
View File
@@ -14,7 +14,9 @@ type SpinGameResponse struct {
// SpinGameResponseData contains the game play result
type SpinGameResponseData struct {
GamePlay GamePlayResponse `json:"game_play"`
PrizeWon *CustomerGamePrizeResponse `json:"prize_won,omitempty"`
TokensRemaining int64 `json:"tokens_remaining"`
GamePlay GamePlayResponse `json:"game_play"`
PrizeWon *CustomerGamePrizeResponse `json:"prize_won,omitempty"`
CoinsRemaining int64 `json:"coins_remaining"`
// Deprecated: the same as CoinsRemaining, kept until the apps read coins_remaining.
TokensRemaining int64 `json:"tokens_remaining"`
}
+31
View File
@@ -0,0 +1,31 @@
package contract
// AdjustCustomerWalletRequest is POST /marketing/customers/:id/wallet/adjust
// (docs/prd-point-coin.md F7).
type AdjustCustomerWalletRequest struct {
// POINT or COIN.
Currency string `json:"currency"`
// Signed: positive adds to the balance, negative takes from it. Never zero.
Amount int64 `json:"amount"`
Reason string `json:"reason"`
// Optional. Sending the same key again returns the first result instead of
// adjusting twice, so a retried request is safe.
IdempotencyKey string `json:"idempotency_key"`
}
// ExchangeCoinsRequest is POST /customer/wallet/exchange (docs/prd-point-coin.md F4).
// The Idempotency-Key header is required.
type ExchangeCoinsRequest struct {
Coins int64 `json:"coins" binding:"required,min=1"`
Pin string `json:"pin" binding:"required"`
}
// TransferWalletRequest is POST /customer/wallet/transfer (docs/prd-point-coin.md F5).
// The Idempotency-Key header is required.
type TransferWalletRequest struct {
// POINT or COIN.
Currency string `json:"currency" binding:"required"`
Amount int64 `json:"amount" binding:"required,min=1"`
RecipientPhone string `json:"recipient_phone" binding:"required"`
Pin string `json:"pin" binding:"required"`
}