Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c18a6788c4 | |||
| 9de7a69756 | |||
| dccfe45019 | |||
| fa4ad25422 | |||
| 5d2460c310 | |||
| e27b2320b4 |
@@ -4316,9 +4316,6 @@
|
|||||||
"discount": {
|
"discount": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"format": "double"
|
"format": "double"
|
||||||
},
|
|
||||||
"promo": {
|
|
||||||
"$ref": "#/definitions/SubscribePromo"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title": "SubscribeDiscount",
|
"title": "SubscribeDiscount",
|
||||||
|
|||||||
@@ -1,37 +1,5 @@
|
|||||||
-- Purpose: Rollback user-level speed limit overrides from user_subscribe
|
-- Purpose: Rollback user-level speed limit overrides from user_subscribe
|
||||||
|
|
||||||
SET @column_exists = (
|
ALTER TABLE `user_subscribe`
|
||||||
SELECT COUNT(*)
|
DROP COLUMN IF EXISTS `traffic_limit`,
|
||||||
FROM INFORMATION_SCHEMA.COLUMNS
|
DROP COLUMN IF EXISTS `speed_limit`;
|
||||||
WHERE TABLE_SCHEMA = DATABASE()
|
|
||||||
AND TABLE_NAME = 'user_subscribe'
|
|
||||||
AND COLUMN_NAME = 'traffic_limit'
|
|
||||||
);
|
|
||||||
|
|
||||||
SET @sql = IF(
|
|
||||||
@column_exists = 1,
|
|
||||||
'ALTER TABLE `user_subscribe` DROP COLUMN `traffic_limit`',
|
|
||||||
'SELECT ''Column traffic_limit does not exist in user_subscribe table'''
|
|
||||||
);
|
|
||||||
|
|
||||||
PREPARE stmt FROM @sql;
|
|
||||||
EXECUTE stmt;
|
|
||||||
DEALLOCATE PREPARE stmt;
|
|
||||||
|
|
||||||
SET @column_exists = (
|
|
||||||
SELECT COUNT(*)
|
|
||||||
FROM INFORMATION_SCHEMA.COLUMNS
|
|
||||||
WHERE TABLE_SCHEMA = DATABASE()
|
|
||||||
AND TABLE_NAME = 'user_subscribe'
|
|
||||||
AND COLUMN_NAME = 'speed_limit'
|
|
||||||
);
|
|
||||||
|
|
||||||
SET @sql = IF(
|
|
||||||
@column_exists = 1,
|
|
||||||
'ALTER TABLE `user_subscribe` DROP COLUMN `speed_limit`',
|
|
||||||
'SELECT ''Column speed_limit does not exist in user_subscribe table'''
|
|
||||||
);
|
|
||||||
|
|
||||||
PREPARE stmt FROM @sql;
|
|
||||||
EXECUTE stmt;
|
|
||||||
DEALLOCATE PREPARE stmt;
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ SET @column_exists = (
|
|||||||
|
|
||||||
SET @sql = IF(
|
SET @sql = IF(
|
||||||
@column_exists = 0,
|
@column_exists = 0,
|
||||||
'ALTER TABLE `user_subscribe` ADD COLUMN `speed_limit` BIGINT NOT NULL DEFAULT 0 COMMENT ''User-level speed limit override (Mbps, 0=use plan default)'' AFTER `upload`',
|
'ALTER TABLE `user_subscribe` ADD COLUMN `speed_limit` int NOT NULL DEFAULT 0 COMMENT ''User-level speed limit override (Mbps, 0=use plan default)'' AFTER `upload`',
|
||||||
'SELECT ''Column speed_limit already exists in user_subscribe table'''
|
'SELECT ''Column speed_limit already exists in user_subscribe table'''
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -14,155 +14,19 @@ CREATE TABLE IF NOT EXISTS `promo_rule` (
|
|||||||
KEY `idx_enabled_priority_deleted` (`enabled`, `deleted_at`, `priority` DESC)
|
KEY `idx_enabled_priority_deleted` (`enabled`, `deleted_at`, `priority` DESC)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='促销规则表';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='促销规则表';
|
||||||
|
|
||||||
SET @index_exists = (
|
|
||||||
SELECT COUNT(*)
|
|
||||||
FROM INFORMATION_SCHEMA.STATISTICS
|
|
||||||
WHERE TABLE_SCHEMA = DATABASE()
|
|
||||||
AND TABLE_NAME = 'promo_rule'
|
|
||||||
AND INDEX_NAME = 'idx_enabled_priority'
|
|
||||||
);
|
|
||||||
|
|
||||||
SET @sql = IF(
|
|
||||||
@index_exists = 1,
|
|
||||||
'ALTER TABLE `promo_rule` DROP INDEX `idx_enabled_priority`',
|
|
||||||
'SELECT ''Index idx_enabled_priority does not exist on promo_rule table'''
|
|
||||||
);
|
|
||||||
|
|
||||||
PREPARE stmt FROM @sql;
|
|
||||||
EXECUTE stmt;
|
|
||||||
DEALLOCATE PREPARE stmt;
|
|
||||||
|
|
||||||
SET @index_exists = (
|
|
||||||
SELECT COUNT(*)
|
|
||||||
FROM INFORMATION_SCHEMA.STATISTICS
|
|
||||||
WHERE TABLE_SCHEMA = DATABASE()
|
|
||||||
AND TABLE_NAME = 'promo_rule'
|
|
||||||
AND INDEX_NAME = 'idx_deleted_at'
|
|
||||||
);
|
|
||||||
|
|
||||||
SET @sql = IF(
|
|
||||||
@index_exists = 1,
|
|
||||||
'ALTER TABLE `promo_rule` DROP INDEX `idx_deleted_at`',
|
|
||||||
'SELECT ''Index idx_deleted_at does not exist on promo_rule table'''
|
|
||||||
);
|
|
||||||
|
|
||||||
PREPARE stmt FROM @sql;
|
|
||||||
EXECUTE stmt;
|
|
||||||
DEALLOCATE PREPARE stmt;
|
|
||||||
|
|
||||||
SET @index_exists = (
|
|
||||||
SELECT COUNT(*)
|
|
||||||
FROM INFORMATION_SCHEMA.STATISTICS
|
|
||||||
WHERE TABLE_SCHEMA = DATABASE()
|
|
||||||
AND TABLE_NAME = 'promo_rule'
|
|
||||||
AND INDEX_NAME = 'idx_enabled_priority_deleted'
|
|
||||||
);
|
|
||||||
|
|
||||||
SET @sql = IF(
|
|
||||||
@index_exists = 0,
|
|
||||||
'ALTER TABLE `promo_rule` ADD KEY `idx_enabled_priority_deleted` (`enabled`, `deleted_at`, `priority` DESC)',
|
|
||||||
'SELECT ''Index idx_enabled_priority_deleted already exists on promo_rule table'''
|
|
||||||
);
|
|
||||||
|
|
||||||
PREPARE stmt FROM @sql;
|
|
||||||
EXECUTE stmt;
|
|
||||||
DEALLOCATE PREPARE stmt;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS `subscribe_promo` (
|
CREATE TABLE IF NOT EXISTS `subscribe_promo` (
|
||||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
`subscribe_id` BIGINT UNSIGNED NOT NULL COMMENT '套餐规格 ID',
|
`subscribe_id` BIGINT UNSIGNED NOT NULL COMMENT '套餐规格 ID',
|
||||||
`quantity` BIGINT NOT NULL DEFAULT 1 COMMENT '购买数量',
|
`quantity` INT NOT NULL DEFAULT 0 COMMENT '购买数量',
|
||||||
`promo_rule_id` BIGINT UNSIGNED NOT NULL COMMENT '促销规则 ID',
|
`promo_rule_id` BIGINT UNSIGNED NOT NULL COMMENT '促销规则 ID',
|
||||||
`promo_price` BIGINT NOT NULL DEFAULT 0 COMMENT '该规格在此规则下的优惠价(分)',
|
`promo_price` BIGINT NOT NULL DEFAULT 0 COMMENT '该规格在此规则下的优惠价(分)',
|
||||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
UNIQUE KEY `uk_subscribe_quantity_rule` (`subscribe_id`, `quantity`, `promo_rule_id`),
|
UNIQUE KEY `uk_subscribe_qty_rule` (`subscribe_id`, `quantity`, `promo_rule_id`),
|
||||||
KEY `idx_promo_rule_id` (`promo_rule_id`)
|
KEY `idx_promo_rule_id` (`promo_rule_id`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='规格促销价表';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='规格促销价表';
|
||||||
|
|
||||||
SET @column_exists = (
|
|
||||||
SELECT COUNT(*)
|
|
||||||
FROM INFORMATION_SCHEMA.COLUMNS
|
|
||||||
WHERE TABLE_SCHEMA = DATABASE()
|
|
||||||
AND TABLE_NAME = 'subscribe_promo'
|
|
||||||
AND COLUMN_NAME = 'quantity'
|
|
||||||
);
|
|
||||||
|
|
||||||
SET @sql = IF(
|
|
||||||
@column_exists = 0,
|
|
||||||
'ALTER TABLE `subscribe_promo` ADD COLUMN `quantity` BIGINT NOT NULL DEFAULT 1 COMMENT ''购买数量'' AFTER `subscribe_id`',
|
|
||||||
'SELECT ''Column quantity already exists in subscribe_promo table'''
|
|
||||||
);
|
|
||||||
|
|
||||||
PREPARE stmt FROM @sql;
|
|
||||||
EXECUTE stmt;
|
|
||||||
DEALLOCATE PREPARE stmt;
|
|
||||||
|
|
||||||
SET @sql = IF(
|
|
||||||
@column_exists = 1,
|
|
||||||
'ALTER TABLE `subscribe_promo` MODIFY COLUMN `quantity` BIGINT NOT NULL DEFAULT 1 COMMENT ''购买数量''',
|
|
||||||
'SELECT ''Column quantity does not exist in subscribe_promo table'''
|
|
||||||
);
|
|
||||||
|
|
||||||
PREPARE stmt FROM @sql;
|
|
||||||
EXECUTE stmt;
|
|
||||||
DEALLOCATE PREPARE stmt;
|
|
||||||
|
|
||||||
SET @index_exists = (
|
|
||||||
SELECT COUNT(*)
|
|
||||||
FROM INFORMATION_SCHEMA.STATISTICS
|
|
||||||
WHERE TABLE_SCHEMA = DATABASE()
|
|
||||||
AND TABLE_NAME = 'subscribe_promo'
|
|
||||||
AND INDEX_NAME = 'uk_subscribe_rule'
|
|
||||||
);
|
|
||||||
|
|
||||||
SET @sql = IF(
|
|
||||||
@index_exists = 1,
|
|
||||||
'ALTER TABLE `subscribe_promo` DROP INDEX `uk_subscribe_rule`',
|
|
||||||
'SELECT ''Index uk_subscribe_rule does not exist on subscribe_promo table'''
|
|
||||||
);
|
|
||||||
|
|
||||||
PREPARE stmt FROM @sql;
|
|
||||||
EXECUTE stmt;
|
|
||||||
DEALLOCATE PREPARE stmt;
|
|
||||||
|
|
||||||
SET @index_exists = (
|
|
||||||
SELECT COUNT(*)
|
|
||||||
FROM INFORMATION_SCHEMA.STATISTICS
|
|
||||||
WHERE TABLE_SCHEMA = DATABASE()
|
|
||||||
AND TABLE_NAME = 'subscribe_promo'
|
|
||||||
AND INDEX_NAME = 'uk_subscribe_qty_rule'
|
|
||||||
);
|
|
||||||
|
|
||||||
SET @sql = IF(
|
|
||||||
@index_exists = 1,
|
|
||||||
'ALTER TABLE `subscribe_promo` DROP INDEX `uk_subscribe_qty_rule`',
|
|
||||||
'SELECT ''Index uk_subscribe_qty_rule does not exist on subscribe_promo table'''
|
|
||||||
);
|
|
||||||
|
|
||||||
PREPARE stmt FROM @sql;
|
|
||||||
EXECUTE stmt;
|
|
||||||
DEALLOCATE PREPARE stmt;
|
|
||||||
|
|
||||||
SET @index_exists = (
|
|
||||||
SELECT COUNT(*)
|
|
||||||
FROM INFORMATION_SCHEMA.STATISTICS
|
|
||||||
WHERE TABLE_SCHEMA = DATABASE()
|
|
||||||
AND TABLE_NAME = 'subscribe_promo'
|
|
||||||
AND INDEX_NAME = 'uk_subscribe_quantity_rule'
|
|
||||||
);
|
|
||||||
|
|
||||||
SET @sql = IF(
|
|
||||||
@index_exists = 0,
|
|
||||||
'ALTER TABLE `subscribe_promo` ADD UNIQUE KEY `uk_subscribe_quantity_rule` (`subscribe_id`, `quantity`, `promo_rule_id`)',
|
|
||||||
'SELECT ''Index uk_subscribe_quantity_rule already exists on subscribe_promo table'''
|
|
||||||
);
|
|
||||||
|
|
||||||
PREPARE stmt FROM @sql;
|
|
||||||
EXECUTE stmt;
|
|
||||||
DEALLOCATE PREPARE stmt;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS `promo_usage` (
|
CREATE TABLE IF NOT EXISTS `promo_usage` (
|
||||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
`user_id` BIGINT UNSIGNED NOT NULL COMMENT '用户 ID',
|
`user_id` BIGINT UNSIGNED NOT NULL COMMENT '用户 ID',
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import (
|
|||||||
"github.com/perfect-panel/server/pkg/xerr"
|
"github.com/perfect-panel/server/pkg/xerr"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"gorm.io/gorm/clause"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type PromoResult struct {
|
type PromoResult struct {
|
||||||
@@ -149,12 +148,7 @@ func evaluateInactiveUserPromo(
|
|||||||
err := db.WithContext(ctx).
|
err := db.WithContext(ctx).
|
||||||
Model(&user.Subscribe{}).
|
Model(&user.Subscribe{}).
|
||||||
Where("user_id = ?", userID).
|
Where("user_id = ?", userID).
|
||||||
Order(clause.OrderBy{
|
Order("expire_time DESC").
|
||||||
Expression: clause.Expr{
|
|
||||||
SQL: "CASE WHEN expire_time = ? THEN 0 ELSE 1 END, expire_time DESC",
|
|
||||||
Vars: []interface{}{time.UnixMilli(0)},
|
|
||||||
},
|
|
||||||
}).
|
|
||||||
Limit(1).
|
Limit(1).
|
||||||
Take(&lastSub).Error
|
Take(&lastSub).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -164,16 +158,8 @@ func evaluateInactiveUserPromo(
|
|||||||
return false, time.Time{}, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query promo inactive user subscription failed")
|
return false, time.Time{}, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query promo inactive user subscription failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
return evaluateInactiveUserExpire(lastSub.ExpireTime, params, now), ruleExpiresAt, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func evaluateInactiveUserExpire(lastExpire time.Time, params promoRuleParams, now time.Time) bool {
|
|
||||||
if lastExpire.Equal(time.UnixMilli(0)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
threshold := now.AddDate(0, -params.InactiveMonths, 0)
|
threshold := now.AddDate(0, -params.InactiveMonths, 0)
|
||||||
return lastExpire.Before(threshold) || lastExpire.Equal(threshold)
|
return lastSub.ExpireTime.Before(threshold) || lastSub.ExpireTime.Equal(threshold), ruleExpiresAt, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func promoRuleExpiresAt(rule *promo.RuleWithPrice) time.Time {
|
func promoRuleExpiresAt(rule *promo.RuleWithPrice) time.Time {
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
package common
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestEvaluateInactiveUserExpire(t *testing.T) {
|
|
||||||
now := time.Date(2026, 5, 27, 8, 0, 0, 0, time.UTC)
|
|
||||||
params := promoRuleParams{InactiveMonths: 3}
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
lastExpire time.Time
|
|
||||||
want bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "permanent subscription is not inactive",
|
|
||||||
lastExpire: time.UnixMilli(0),
|
|
||||||
want: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "active subscription is not inactive",
|
|
||||||
lastExpire: now.Add(time.Hour),
|
|
||||||
want: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "expire at threshold is inactive",
|
|
||||||
lastExpire: now.AddDate(0, -3, 0),
|
|
||||||
want: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "expire before threshold is inactive",
|
|
||||||
lastExpire: now.AddDate(0, -3, -1),
|
|
||||||
want: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
if got := evaluateInactiveUserExpire(tt.lastExpire, params, now); got != tt.want {
|
|
||||||
t.Fatalf("evaluateInactiveUserExpire() = %v, want %v", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -47,18 +47,13 @@ func (l *PreCreateOrderLogic) PreCreateOrder(req *types.PurchaseOrderRequest) (r
|
|||||||
l.Debugf("[PreCreateOrder] Quantity is less than or equal to 0, setting to 1")
|
l.Debugf("[PreCreateOrder] Quantity is less than or equal to 0, setting to 1")
|
||||||
req.Quantity = 1
|
req.Quantity = 1
|
||||||
}
|
}
|
||||||
entitlement, entErr := commonLogic.ResolveEntitlementUser(l.ctx, l.svcCtx.DB, u.Id)
|
|
||||||
if entErr != nil {
|
|
||||||
return nil, entErr
|
|
||||||
}
|
|
||||||
|
|
||||||
targetSubscribeID := req.SubscribeId
|
targetSubscribeID := req.SubscribeId
|
||||||
orderType := uint8(1)
|
|
||||||
isSingleModeRenewal := false
|
isSingleModeRenewal := false
|
||||||
decision, routeErr := commonLogic.ResolvePurchaseRoute(
|
decision, routeErr := commonLogic.ResolvePurchaseRoute(
|
||||||
l.ctx,
|
l.ctx,
|
||||||
l.svcCtx.Config.Subscribe.SingleModel,
|
l.svcCtx.Config.Subscribe.SingleModel,
|
||||||
entitlement.EffectiveUserID,
|
u.Id,
|
||||||
req.SubscribeId,
|
req.SubscribeId,
|
||||||
l.svcCtx.UserModel.FindSingleModeAnchorSubscribe,
|
l.svcCtx.UserModel.FindSingleModeAnchorSubscribe,
|
||||||
)
|
)
|
||||||
@@ -73,44 +68,15 @@ func (l *PreCreateOrderLogic) PreCreateOrder(req *types.PurchaseOrderRequest) (r
|
|||||||
targetSubscribeID = decision.ResolvedSubscribeID
|
targetSubscribeID = decision.ResolvedSubscribeID
|
||||||
isSingleModeRenewal = decision.Route == commonLogic.PurchaseRoutePurchaseToRenewal
|
isSingleModeRenewal = decision.Route == commonLogic.PurchaseRoutePurchaseToRenewal
|
||||||
if isSingleModeRenewal && decision.Anchor != nil {
|
if isSingleModeRenewal && decision.Anchor != nil {
|
||||||
orderType = 2
|
|
||||||
l.Infow("[PreCreateOrder] single mode purchase routed to renewal preview",
|
l.Infow("[PreCreateOrder] single mode purchase routed to renewal preview",
|
||||||
logger.Field("mode", "single"),
|
logger.Field("mode", "single"),
|
||||||
logger.Field("route", "purchase_to_renewal"),
|
logger.Field("route", "purchase_to_renewal"),
|
||||||
logger.Field("anchor_user_subscribe_id", decision.Anchor.Id),
|
logger.Field("anchor_user_subscribe_id", decision.Anchor.Id),
|
||||||
logger.Field("user_id", u.Id),
|
logger.Field("user_id", u.Id),
|
||||||
logger.Field("effective_user_id", entitlement.EffectiveUserID),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep promo eligibility preview aligned with Purchase: an existing paid subscription
|
|
||||||
// routes the request to renewal semantics, where first-purchase promos are disabled.
|
|
||||||
if !l.svcCtx.Config.Subscribe.SingleModel && orderType == 1 {
|
|
||||||
var existSub user.Subscribe
|
|
||||||
if e := l.svcCtx.DB.WithContext(l.ctx).
|
|
||||||
Model(&user.Subscribe{}).
|
|
||||||
Where("user_id = ? AND token != '' AND (order_id > 0 OR token LIKE 'iap:%')", entitlement.EffectiveUserID).
|
|
||||||
Order("expire_time DESC").
|
|
||||||
Order("updated_at DESC").
|
|
||||||
Order("id DESC").
|
|
||||||
First(&existSub).Error; e == nil && existSub.Id > 0 && existSub.Token != "" {
|
|
||||||
orderType = 2
|
|
||||||
l.Infow("[PreCreateOrder] purchase preview routed to renewal because an existing subscription was found",
|
|
||||||
logger.Field("route_mode", "global_single_subscription"),
|
|
||||||
logger.Field("route", "purchase_to_existing_subscription"),
|
|
||||||
logger.Field("existing_subscribe_id", existSub.Id),
|
|
||||||
logger.Field("existing_status", existSub.Status),
|
|
||||||
logger.Field("user_id", u.Id),
|
|
||||||
logger.Field("effective_user_id", entitlement.EffectiveUserID),
|
|
||||||
logger.Field("resolved_subscribe_id", targetSubscribeID),
|
|
||||||
)
|
|
||||||
} else if e != nil && !errors.Is(e, gorm.ErrRecordNotFound) {
|
|
||||||
l.Errorw("[PreCreateOrder] Database query error", logger.Field("error", e.Error()), logger.Field("user_id", u.Id))
|
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find existing subscription error: %v", e.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// find subscribe plan
|
// find subscribe plan
|
||||||
sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, targetSubscribeID)
|
sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, targetSubscribeID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -120,7 +86,7 @@ func (l *PreCreateOrderLogic) PreCreateOrder(req *types.PurchaseOrderRequest) (r
|
|||||||
|
|
||||||
// check subscribe plan quota limit for new purchase flow only
|
// check subscribe plan quota limit for new purchase flow only
|
||||||
if !isSingleModeRenewal && sub.Quota > 0 {
|
if !isSingleModeRenewal && sub.Quota > 0 {
|
||||||
userSub, err := l.svcCtx.UserModel.QueryUserSubscribe(l.ctx, entitlement.EffectiveUserID)
|
userSub, err := l.svcCtx.UserModel.QueryUserSubscribe(l.ctx, u.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Errorw("[PreCreateOrder] Database query error", logger.Field("error", err.Error()), logger.Field("user_id", u.Id))
|
l.Errorw("[PreCreateOrder] Database query error", logger.Field("error", err.Error()), logger.Field("user_id", u.Id))
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find user subscription error: %v", err.Error())
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find user subscription error: %v", err.Error())
|
||||||
@@ -136,7 +102,7 @@ func (l *PreCreateOrderLogic) PreCreateOrder(req *types.PurchaseOrderRequest) (r
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
newUserDiscount, err := resolveNewUserDiscountEligibility(l.ctx, l.svcCtx.DB, entitlement.EffectiveUserID, targetSubscribeID, req.Quantity, sub.Discount)
|
newUserDiscount, err := resolveNewUserDiscountEligibility(l.ctx, l.svcCtx.DB, u.Id, targetSubscribeID, req.Quantity, sub.Discount)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Errorw("[PreCreateOrder] Database query error resolving new user eligibility",
|
l.Errorw("[PreCreateOrder] Database query error resolving new user eligibility",
|
||||||
logger.Field("error", err.Error()),
|
logger.Field("error", err.Error()),
|
||||||
@@ -151,13 +117,13 @@ func (l *PreCreateOrderLogic) PreCreateOrder(req *types.PurchaseOrderRequest) (r
|
|||||||
priceResult, err := calculatePurchasePrice(
|
priceResult, err := calculatePurchasePrice(
|
||||||
l.ctx,
|
l.ctx,
|
||||||
l.svcCtx,
|
l.svcCtx,
|
||||||
entitlement.EffectiveUserID,
|
u.Id,
|
||||||
targetSubscribeID,
|
targetSubscribeID,
|
||||||
sub.UnitPrice,
|
sub.UnitPrice,
|
||||||
req.Quantity,
|
req.Quantity,
|
||||||
newUserDiscount.Discounts,
|
newUserDiscount.Discounts,
|
||||||
newUserDiscount.EligibleForDiscount,
|
newUserDiscount.EligibleForDiscount,
|
||||||
orderType == 1,
|
!isSingleModeRenewal,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Errorw("[PreCreateOrder] Promo price calculation error",
|
l.Errorw("[PreCreateOrder] Promo price calculation error",
|
||||||
|
|||||||
@@ -11,85 +11,75 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type fakePromoModel struct {
|
type fakePromoModel struct {
|
||||||
rules []*promo.RuleWithPrice
|
rules []*promo.RuleWithPrice
|
||||||
lastSubscribeID int64
|
|
||||||
lastQuantity int64
|
|
||||||
requireQuantity int64
|
|
||||||
quantityMismatch []*promo.RuleWithPrice
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *fakePromoModel) QueryEligibleRules(_ context.Context, subscribeID int64, quantity int64) ([]*promo.RuleWithPrice, error) {
|
func (m fakePromoModel) QueryEligibleRules(context.Context, int64, int64) ([]*promo.RuleWithPrice, error) {
|
||||||
m.lastSubscribeID = subscribeID
|
|
||||||
m.lastQuantity = quantity
|
|
||||||
if m.requireQuantity > 0 && quantity != m.requireQuantity {
|
|
||||||
return m.quantityMismatch, nil
|
|
||||||
}
|
|
||||||
return m.rules, nil
|
return m.rules, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *fakePromoModel) InsertUsage(context.Context, *promo.Usage, ...*gorm.DB) error {
|
func (m fakePromoModel) InsertUsage(context.Context, *promo.Usage, ...*gorm.DB) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *fakePromoModel) InsertRule(context.Context, *promo.Rule) error {
|
func (m fakePromoModel) InsertRule(context.Context, *promo.Rule) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *fakePromoModel) FindRule(context.Context, int64) (*promo.Rule, error) {
|
func (m fakePromoModel) FindRule(context.Context, int64) (*promo.Rule, error) {
|
||||||
return nil, gorm.ErrRecordNotFound
|
return nil, gorm.ErrRecordNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *fakePromoModel) UpdateRule(context.Context, *promo.Rule) error {
|
func (m fakePromoModel) UpdateRule(context.Context, *promo.Rule) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *fakePromoModel) DeleteRule(context.Context, int64) error {
|
func (m fakePromoModel) DeleteRule(context.Context, int64) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *fakePromoModel) QueryRuleList(context.Context, int, int, string, *bool, string) (int64, []*promo.Rule, error) {
|
func (m fakePromoModel) QueryRuleList(context.Context, int, int, string, *bool, string) (int64, []*promo.Rule, error) {
|
||||||
return 0, nil, nil
|
return 0, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *fakePromoModel) UpsertPrices(context.Context, int64, []*promo.SubscribePromo) error {
|
func (m fakePromoModel) UpsertPrices(context.Context, int64, []*promo.SubscribePromo) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *fakePromoModel) FindPrice(context.Context, int64) (*promo.SubscribePromo, error) {
|
func (m fakePromoModel) FindPrice(context.Context, int64) (*promo.SubscribePromo, error) {
|
||||||
return nil, gorm.ErrRecordNotFound
|
return nil, gorm.ErrRecordNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *fakePromoModel) DeletePrice(context.Context, int64) error {
|
func (m fakePromoModel) DeletePrice(context.Context, int64) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *fakePromoModel) QueryPriceList(context.Context, int64, int, int) (int64, []*promo.SubscribePromo, error) {
|
func (m fakePromoModel) QueryPriceList(context.Context, int64, int, int) (int64, []*promo.SubscribePromo, error) {
|
||||||
return 0, nil, nil
|
return 0, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *fakePromoModel) QueryUsageList(context.Context, promo.UsageFilter) (int64, []*promo.Usage, error) {
|
func (m fakePromoModel) QueryUsageList(context.Context, promo.UsageFilter) (int64, []*promo.Usage, error) {
|
||||||
return 0, nil, nil
|
return 0, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *fakePromoModel) Transaction(context.Context, func(*gorm.DB) error) error {
|
func (m fakePromoModel) Transaction(context.Context, func(*gorm.DB) error) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCalculatePurchasePricePromoUsesQuantityTierTotalPrice(t *testing.T) {
|
func TestCalculatePurchasePricePromoUsesQuantityTierTotalPrice(t *testing.T) {
|
||||||
model := &fakePromoModel{rules: []*promo.RuleWithPrice{
|
|
||||||
{
|
|
||||||
Rule: promo.Rule{
|
|
||||||
Id: 9,
|
|
||||||
Name: "campaign",
|
|
||||||
Type: promo.RuleTypeCampaign,
|
|
||||||
Enabled: true,
|
|
||||||
},
|
|
||||||
PromoPrice: 279,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
svcCtx := &svc.ServiceContext{
|
svcCtx := &svc.ServiceContext{
|
||||||
DB: &gorm.DB{},
|
DB: &gorm.DB{},
|
||||||
PromoModel: model,
|
PromoModel: fakePromoModel{rules: []*promo.RuleWithPrice{
|
||||||
|
{
|
||||||
|
Rule: promo.Rule{
|
||||||
|
Id: 9,
|
||||||
|
Name: "campaign",
|
||||||
|
Type: promo.RuleTypeCampaign,
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
PromoPrice: 279,
|
||||||
|
},
|
||||||
|
}},
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := calculatePurchasePrice(
|
result, err := calculatePurchasePrice(
|
||||||
@@ -125,26 +115,22 @@ func TestCalculatePurchasePricePromoUsesQuantityTierTotalPrice(t *testing.T) {
|
|||||||
if result.PromoPrice != 279 {
|
if result.PromoPrice != 279 {
|
||||||
t.Fatalf("PromoPrice = %d, want 279", result.PromoPrice)
|
t.Fatalf("PromoPrice = %d, want 279", result.PromoPrice)
|
||||||
}
|
}
|
||||||
if model.lastQuantity != 7 {
|
|
||||||
t.Fatalf("promo query quantity = %d, want 7", model.lastQuantity)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCalculatePurchasePriceIgnoresInvalidPromoPrice(t *testing.T) {
|
func TestCalculatePurchasePriceIgnoresInvalidPromoPrice(t *testing.T) {
|
||||||
model := &fakePromoModel{rules: []*promo.RuleWithPrice{
|
|
||||||
{
|
|
||||||
Rule: promo.Rule{
|
|
||||||
Id: 10,
|
|
||||||
Name: "invalid campaign",
|
|
||||||
Type: promo.RuleTypeCampaign,
|
|
||||||
Enabled: true,
|
|
||||||
},
|
|
||||||
PromoPrice: 3000,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
svcCtx := &svc.ServiceContext{
|
svcCtx := &svc.ServiceContext{
|
||||||
DB: &gorm.DB{},
|
DB: &gorm.DB{},
|
||||||
PromoModel: model,
|
PromoModel: fakePromoModel{rules: []*promo.RuleWithPrice{
|
||||||
|
{
|
||||||
|
Rule: promo.Rule{
|
||||||
|
Id: 10,
|
||||||
|
Name: "invalid campaign",
|
||||||
|
Type: promo.RuleTypeCampaign,
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
PromoPrice: 3000,
|
||||||
|
},
|
||||||
|
}},
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := calculatePurchasePrice(
|
result, err := calculatePurchasePrice(
|
||||||
@@ -172,52 +158,3 @@ func TestCalculatePurchasePriceIgnoresInvalidPromoPrice(t *testing.T) {
|
|||||||
t.Fatalf("promo fields = (%d, %d), want (0, 0)", result.PromoRuleId, result.PromoDiscount)
|
t.Fatalf("promo fields = (%d, %d), want (0, 0)", result.PromoRuleId, result.PromoDiscount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCalculatePurchasePricePassesQuantityToPromoEvaluation(t *testing.T) {
|
|
||||||
promoModel := &fakePromoModel{
|
|
||||||
requireQuantity: 6,
|
|
||||||
rules: []*promo.RuleWithPrice{
|
|
||||||
{
|
|
||||||
Rule: promo.Rule{
|
|
||||||
Id: 11,
|
|
||||||
Name: "quantity campaign",
|
|
||||||
Type: promo.RuleTypeCampaign,
|
|
||||||
Enabled: true,
|
|
||||||
},
|
|
||||||
PromoPrice: 3000,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
svcCtx := &svc.ServiceContext{
|
|
||||||
DB: &gorm.DB{},
|
|
||||||
PromoModel: promoModel,
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := calculatePurchasePrice(
|
|
||||||
context.Background(),
|
|
||||||
svcCtx,
|
|
||||||
1,
|
|
||||||
2,
|
|
||||||
1000,
|
|
||||||
6,
|
|
||||||
[]types.SubscribeDiscount{{Quantity: 6, Discount: 80}},
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("calculatePurchasePrice returned error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if promoModel.lastSubscribeID != 2 {
|
|
||||||
t.Fatalf("lastSubscribeID = %d, want 2", promoModel.lastSubscribeID)
|
|
||||||
}
|
|
||||||
if promoModel.lastQuantity != 6 {
|
|
||||||
t.Fatalf("lastQuantity = %d, want 6", promoModel.lastQuantity)
|
|
||||||
}
|
|
||||||
if result.PayableBase != 3000 {
|
|
||||||
t.Fatalf("PayableBase = %d, want 3000", result.PayableBase)
|
|
||||||
}
|
|
||||||
if result.PromoRuleId != 11 {
|
|
||||||
t.Fatalf("PromoRuleId = %d, want 11", result.PromoRuleId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import (
|
|||||||
"github.com/perfect-panel/server/pkg/xerr"
|
"github.com/perfect-panel/server/pkg/xerr"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"gorm.io/gorm/clause"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -58,12 +57,6 @@ func loadSubscribePromoMap(ctx context.Context, svcCtx *svc.ServiceContext, subs
|
|||||||
evaluator := promoEligibilityEvaluator{ctx: ctx, db: svcCtx.DB, userInfo: userInfo}
|
evaluator := promoEligibilityEvaluator{ctx: ctx, db: svcCtx.DB, userInfo: userInfo}
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
for _, candidate := range candidates {
|
for _, candidate := range candidates {
|
||||||
if candidate.Quantity <= 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if result[candidate.SubscribeId] == nil {
|
|
||||||
result[candidate.SubscribeId] = make(map[int64]*types.SubscribePromo)
|
|
||||||
}
|
|
||||||
if _, exists := result[candidate.SubscribeId][candidate.Quantity]; exists {
|
if _, exists := result[candidate.SubscribeId][candidate.Quantity]; exists {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -77,6 +70,9 @@ func loadSubscribePromoMap(ctx context.Context, svcCtx *svc.ServiceContext, subs
|
|||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if _, exists := result[candidate.SubscribeId]; !exists {
|
||||||
|
result[candidate.SubscribeId] = make(map[int64]*types.SubscribePromo)
|
||||||
|
}
|
||||||
result[candidate.SubscribeId][candidate.Quantity] = &types.SubscribePromo{
|
result[candidate.SubscribeId][candidate.Quantity] = &types.SubscribePromo{
|
||||||
RuleName: candidate.RuleName,
|
RuleName: candidate.RuleName,
|
||||||
RuleType: candidate.RuleType,
|
RuleType: candidate.RuleType,
|
||||||
@@ -90,16 +86,7 @@ func loadSubscribePromoMap(ctx context.Context, svcCtx *svc.ServiceContext, subs
|
|||||||
|
|
||||||
func querySubscribePromoCandidates(ctx context.Context, svcCtx *svc.ServiceContext, subscribeIDs []int64, loggedIn bool) ([]subscribePromoCandidate, error) {
|
func querySubscribePromoCandidates(ctx context.Context, svcCtx *svc.ServiceContext, subscribeIDs []int64, loggedIn bool) ([]subscribePromoCandidate, error) {
|
||||||
var candidates []subscribePromoCandidate
|
var candidates []subscribePromoCandidate
|
||||||
err := subscribePromoCandidatesQuery(ctx, svcCtx.DB, subscribeIDs, loggedIn).
|
query := svcCtx.DB.WithContext(ctx).
|
||||||
Scan(&candidates).Error
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query subscribe promo candidates failed: %v", err)
|
|
||||||
}
|
|
||||||
return candidates, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func subscribePromoCandidatesQuery(ctx context.Context, db *gorm.DB, subscribeIDs []int64, loggedIn bool) *gorm.DB {
|
|
||||||
query := db.WithContext(ctx).
|
|
||||||
Table("subscribe_promo AS sp").
|
Table("subscribe_promo AS sp").
|
||||||
Select("sp.subscribe_id, sp.quantity, sp.promo_price, pr.name AS rule_name, pr.type AS rule_type, pr.params, pr.start_time, pr.end_time").
|
Select("sp.subscribe_id, sp.quantity, sp.promo_price, pr.name AS rule_name, pr.type AS rule_type, pr.params, pr.start_time, pr.end_time").
|
||||||
Joins("JOIN promo_rule AS pr ON pr.id = sp.promo_rule_id AND pr.deleted_at IS NULL").
|
Joins("JOIN promo_rule AS pr ON pr.id = sp.promo_rule_id AND pr.deleted_at IS NULL").
|
||||||
@@ -107,11 +94,16 @@ func subscribePromoCandidatesQuery(ctx context.Context, db *gorm.DB, subscribeID
|
|||||||
if !loggedIn {
|
if !loggedIn {
|
||||||
query = query.Where("pr.type = ?", promoRuleTypeCampaign)
|
query = query.Where("pr.type = ?", promoRuleTypeCampaign)
|
||||||
}
|
}
|
||||||
return query.
|
err := query.
|
||||||
Order("sp.subscribe_id ASC").
|
Order("sp.subscribe_id ASC").
|
||||||
Order("sp.quantity ASC").
|
Order("sp.quantity ASC").
|
||||||
Order("pr.priority DESC").
|
Order("pr.priority DESC").
|
||||||
Order("pr.id ASC")
|
Order("pr.id ASC").
|
||||||
|
Scan(&candidates).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query subscribe promo candidates failed: %v", err)
|
||||||
|
}
|
||||||
|
return candidates, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c subscribePromoCandidate) isActive(now time.Time) bool {
|
func (c subscribePromoCandidate) isActive(now time.Time) bool {
|
||||||
@@ -184,7 +176,11 @@ func (e *promoEligibilityEvaluator) lastSubscribeExpireAt() (time.Time, error) {
|
|||||||
return *e.lastExpire, nil
|
return *e.lastExpire, nil
|
||||||
}
|
}
|
||||||
var item user.Subscribe
|
var item user.Subscribe
|
||||||
err := e.lastSubscribeExpireQuery().
|
err := e.db.WithContext(e.ctx).
|
||||||
|
Model(&user.Subscribe{}).
|
||||||
|
Where("user_id = ?", e.userInfo.Id).
|
||||||
|
Where("expire_time != ?", time.UnixMilli(0)).
|
||||||
|
Order("expire_time DESC").
|
||||||
Limit(1).
|
Limit(1).
|
||||||
Take(&item).Error
|
Take(&item).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -199,18 +195,6 @@ func (e *promoEligibilityEvaluator) lastSubscribeExpireAt() (time.Time, error) {
|
|||||||
return item.ExpireTime, nil
|
return item.ExpireTime, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *promoEligibilityEvaluator) lastSubscribeExpireQuery() *gorm.DB {
|
|
||||||
return e.db.WithContext(e.ctx).
|
|
||||||
Model(&user.Subscribe{}).
|
|
||||||
Where("user_id = ?", e.userInfo.Id).
|
|
||||||
Order(clause.OrderBy{
|
|
||||||
Expression: clause.Expr{
|
|
||||||
SQL: "CASE WHEN expire_time = ? THEN 0 ELSE 1 END, expire_time DESC",
|
|
||||||
Vars: []interface{}{time.UnixMilli(0)},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c subscribePromoCandidate) expiresAt() time.Time {
|
func (c subscribePromoCandidate) expiresAt() time.Time {
|
||||||
if c.EndTime == nil {
|
if c.EndTime == nil {
|
||||||
return time.Time{}
|
return time.Time{}
|
||||||
|
|||||||
@@ -1,15 +1,10 @@
|
|||||||
package subscribe
|
package subscribe
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/perfect-panel/server/internal/model/user"
|
"github.com/perfect-panel/server/internal/model/user"
|
||||||
"github.com/perfect-panel/server/internal/types"
|
|
||||||
"gorm.io/driver/mysql"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPromoEligibilityEvaluatorMatch(t *testing.T) {
|
func TestPromoEligibilityEvaluatorMatch(t *testing.T) {
|
||||||
@@ -78,83 +73,3 @@ func TestSubscribePromoCandidateActiveWindow(t *testing.T) {
|
|||||||
t.Fatal("candidate after end time should not be active")
|
t.Fatal("candidate after end time should not be active")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLastSubscribeExpireAtPrioritizesPermanentSubscription(t *testing.T) {
|
|
||||||
db, err := gorm.Open(mysql.New(mysql.Config{
|
|
||||||
DSN: "gorm:gorm@tcp(localhost:9910)/gorm?charset=utf8&parseTime=True&loc=Local",
|
|
||||||
SkipInitializeWithVersion: true,
|
|
||||||
}), &gorm.Config{DryRun: true, DisableAutomaticPing: true})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("open dry-run db: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
evaluator := &promoEligibilityEvaluator{
|
|
||||||
db: db,
|
|
||||||
userInfo: &user.User{Id: 7},
|
|
||||||
}
|
|
||||||
var item user.Subscribe
|
|
||||||
tx := evaluator.lastSubscribeExpireQuery().Limit(1).Take(&item)
|
|
||||||
|
|
||||||
sql := tx.Statement.SQL.String()
|
|
||||||
if !strings.Contains(sql, "CASE WHEN expire_time = ? THEN 0 ELSE 1 END") {
|
|
||||||
t.Fatalf("SQL missing permanent subscription priority order: %s", sql)
|
|
||||||
}
|
|
||||||
if strings.Contains(sql, "expire_time !=") {
|
|
||||||
t.Fatalf("SQL should not filter out permanent subscriptions: %s", sql)
|
|
||||||
}
|
|
||||||
if len(tx.Statement.Vars) < 2 {
|
|
||||||
t.Fatalf("SQL vars length = %d, want at least 2; vars=%v", len(tx.Statement.Vars), tx.Statement.Vars)
|
|
||||||
}
|
|
||||||
if got, want := tx.Statement.Vars[1], time.UnixMilli(0); got != want {
|
|
||||||
t.Fatalf("permanent subscription order var = %v, want %v; vars=%v", got, want, tx.Statement.Vars)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestQuerySubscribePromoCandidatesIncludesQuantity(t *testing.T) {
|
|
||||||
db, err := gorm.Open(mysql.New(mysql.Config{
|
|
||||||
DSN: "gorm:gorm@tcp(localhost:9910)/gorm?charset=utf8&parseTime=True&loc=Local",
|
|
||||||
SkipInitializeWithVersion: true,
|
|
||||||
}), &gorm.Config{DryRun: true, DisableAutomaticPing: true})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("open dry-run db: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var candidates []subscribePromoCandidate
|
|
||||||
tx := subscribePromoCandidatesQuery(context.Background(), db, []int64{11, 12}, true).Scan(&candidates)
|
|
||||||
stmt := tx.Statement
|
|
||||||
sql := stmt.SQL.String()
|
|
||||||
if !strings.Contains(sql, "sp.subscribe_id, sp.quantity, sp.promo_price") {
|
|
||||||
t.Fatalf("SQL missing quantity select: %s", sql)
|
|
||||||
}
|
|
||||||
if !strings.Contains(sql, "ORDER BY sp.subscribe_id ASC,sp.quantity ASC,pr.priority DESC,pr.id ASC") {
|
|
||||||
t.Fatalf("SQL missing quantity order: %s", sql)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestApplySubscribeDiscountPromosMatchesQuantity(t *testing.T) {
|
|
||||||
subscribe := types.Subscribe{Discount: []types.SubscribeDiscount{
|
|
||||||
{Quantity: 1},
|
|
||||||
{Quantity: 12},
|
|
||||||
}}
|
|
||||||
promos := map[int64]*types.SubscribePromo{
|
|
||||||
3: {RuleName: "季度优惠", PromoPrice: 2900},
|
|
||||||
12: {RuleName: "年度优惠", PromoPrice: 9900},
|
|
||||||
}
|
|
||||||
|
|
||||||
applySubscribeDiscountPromos(&subscribe, promos)
|
|
||||||
if subscribe.Discount[0].Promo != nil {
|
|
||||||
t.Fatalf("quantity 1 promo should be nil, got %+v", subscribe.Discount[0].Promo)
|
|
||||||
}
|
|
||||||
if subscribe.Discount[1].Promo == nil {
|
|
||||||
t.Fatal("quantity 12 promo should match")
|
|
||||||
}
|
|
||||||
if got, want := subscribe.Discount[1].Promo.RuleName, "年度优惠"; got != want {
|
|
||||||
t.Fatalf("promo rule name = %q, want %q", got, want)
|
|
||||||
}
|
|
||||||
|
|
||||||
subscribe = types.Subscribe{Discount: []types.SubscribeDiscount{{Quantity: 6}}}
|
|
||||||
applySubscribeDiscountPromos(&subscribe, promos)
|
|
||||||
if subscribe.Discount[0].Promo != nil {
|
|
||||||
t.Fatalf("promo should be nil when quantity does not match, got %+v", subscribe.Discount[0].Promo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3680,9 +3680,6 @@
|
|||||||
"discount": {
|
"discount": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"format": "double"
|
"format": "double"
|
||||||
},
|
|
||||||
"promo": {
|
|
||||||
"$ref": "#/definitions/SubscribePromo"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title": "SubscribeDiscount",
|
"title": "SubscribeDiscount",
|
||||||
|
|||||||
@@ -149,20 +149,7 @@ func (l *ActivateOrderLogic) ProcessTask(ctx context.Context, task *asynq.Task)
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = l.recordPromoUsage(ctx, orderInfo); err != nil {
|
l.recordPromoUsage(ctx, orderInfo)
|
||||||
if releaseErr := l.releaseClaim(ctx, orderInfo.OrderNo); releaseErr != nil {
|
|
||||||
logger.WithContext(ctx).Error("[ActivateOrderLogic] releaseClaim also failed, stuck recovery will handle",
|
|
||||||
logger.Field("order_no", orderInfo.OrderNo),
|
|
||||||
logger.Field("release_error", releaseErr.Error()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
logger.WithContext(ctx).Error("[ActivateOrderLogic] 促销使用记录写入失败,将重试",
|
|
||||||
logger.Field("order_no", orderInfo.OrderNo),
|
|
||||||
logger.Field("promo_rule_id", orderInfo.PromoRuleId),
|
|
||||||
logger.Field("error", err.Error()),
|
|
||||||
)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
l.finalizeCouponAndOrder(ctx, orderInfo)
|
l.finalizeCouponAndOrder(ctx, orderInfo)
|
||||||
|
|
||||||
commonLogic.SubscriptionTraceInfo(logger.WithContext(ctx), commonLogic.SubscriptionTraceFlowOrder, "activation_finished",
|
commonLogic.SubscriptionTraceInfo(logger.WithContext(ctx), commonLogic.SubscriptionTraceFlowOrder, "activation_finished",
|
||||||
@@ -172,9 +159,9 @@ func (l *ActivateOrderLogic) ProcessTask(ctx context.Context, task *asynq.Task)
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *ActivateOrderLogic) recordPromoUsage(ctx context.Context, orderInfo *order.Order) error {
|
func (l *ActivateOrderLogic) recordPromoUsage(ctx context.Context, orderInfo *order.Order) {
|
||||||
if orderInfo == nil || orderInfo.PromoRuleId <= 0 || orderInfo.Quantity <= 0 || orderInfo.SubscribeId <= 0 || orderInfo.OrderNo == "" {
|
if orderInfo == nil || orderInfo.PromoRuleId <= 0 || orderInfo.Quantity <= 0 || orderInfo.SubscribeId <= 0 || orderInfo.OrderNo == "" {
|
||||||
return nil
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
promoPrice := int64(0)
|
promoPrice := int64(0)
|
||||||
@@ -182,10 +169,10 @@ func (l *ActivateOrderLogic) recordPromoUsage(ctx context.Context, orderInfo *or
|
|||||||
promoPrice = (orderInfo.Price - orderInfo.PromoDiscount) / orderInfo.Quantity
|
promoPrice = (orderInfo.Price - orderInfo.PromoDiscount) / orderInfo.Quantity
|
||||||
}
|
}
|
||||||
if promoPrice <= 0 {
|
if promoPrice <= 0 {
|
||||||
return nil
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
return l.svc.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
err := l.svc.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
var count int64
|
var count int64
|
||||||
if e := tx.Model(&promo.Usage{}).Where("order_no = ?", orderInfo.OrderNo).Count(&count).Error; e != nil {
|
if e := tx.Model(&promo.Usage{}).Where("order_no = ?", orderInfo.OrderNo).Count(&count).Error; e != nil {
|
||||||
return e
|
return e
|
||||||
@@ -201,6 +188,13 @@ func (l *ActivateOrderLogic) recordPromoUsage(ctx context.Context, orderInfo *or
|
|||||||
PromoPrice: promoPrice,
|
PromoPrice: promoPrice,
|
||||||
}, tx)
|
}, tx)
|
||||||
})
|
})
|
||||||
|
if err != nil {
|
||||||
|
logger.WithContext(ctx).Error("Insert promo usage failed",
|
||||||
|
logger.Field("error", err.Error()),
|
||||||
|
logger.Field("order_no", orderInfo.OrderNo),
|
||||||
|
logger.Field("promo_rule_id", orderInfo.PromoRuleId),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// parsePayload unMarshals the task payload into a structured format
|
// parsePayload unMarshals the task payload into a structured format
|
||||||
|
|||||||
Reference in New Issue
Block a user