Files
iotlcm/admin-svr/server/internal/logic/iot/simcardbatch/change/complete.go
T

516 lines
18 KiB
Go

package change
import (
"context"
"encoding/json"
"fmt"
"strings"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/logic/iot/cardpackage"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/do"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
)
func (p *Processor) Complete(ctx context.Context, tx gdb.TX, in *CompleteInput) (*CompleteOutput, error) {
if in == nil || in.SubmitParam == nil {
return nil, gerror.New("change params cannot be empty")
}
sourceBatchNo := strings.TrimSpace(in.SourceBatchNo)
if strings.TrimSpace(in.BatchNo) == "" {
return nil, gerror.New("change batchNo cannot be empty")
}
if len(in.Rows) == 0 {
return nil, gerror.New("change cards cannot be empty")
}
if in.PriceSnapshot == nil {
return nil, gerror.New("change price snapshot cannot be empty")
}
if in.PackageBeginTime == nil {
return nil, gerror.New("change package begin time cannot be empty")
}
iccids := rowsToIccids(in.Rows)
packageSvc := cardpackage.NewService()
termination, err := packageSvc.TerminateCurrentPackage(ctx, tx, &cardpackage.TerminateCurrentPackageInput{
BatchNo: in.BatchNo,
SourceBatchNo: sourceBatchNo,
Iccids: iccids,
Operator: in.Operator,
})
if err != nil {
return nil, err
}
voidedByIccid, err := p.voidFuturePendingBinds(ctx, tx, iccids, in.BatchNo, termination.UnsubTime, in.Operator.UserId)
if err != nil {
return nil, err
}
quantity := len(iccids)
totalAmount := roundAmount(in.PriceSnapshot.ChangeUnitPrice * float64(in.SubmitParam.DurationMonths) * float64(quantity))
cards := make([]cardpackage.SubscribeCard, 0, quantity)
for _, iccid := range iccids {
card := termination.CardsByIccid[iccid]
if card == nil {
return nil, gerror.Newf("ICCID card info not found: %s", iccid)
}
cards = append(cards, cardpackage.SubscribeCard{
Iccid: iccid,
Msisdn: strings.TrimSpace(card.Msisdn),
Imsi: strings.TrimSpace(card.Imsi),
})
}
subscribe, err := packageSvc.CreatePackageSubscription(ctx, tx, &cardpackage.CreatePackageSubscriptionInput{
BatchNo: in.BatchNo,
Cards: cards,
CustomerAgentId: in.PriceSnapshot.CustomerAgentId,
PackageCode: in.PriceSnapshot.NewPackageCode,
PackageBeginTime: in.PackageBeginTime,
DurationMonths: in.SubmitParam.DurationMonths,
GiftDurationMonths: in.SubmitParam.GiftDurationMonths,
Amount: fmt.Sprintf("%.2f", totalAmount),
OfflineContractCompleted: in.SubmitParam.OfflineContractCompleted,
BuyerNote: in.SubmitParam.BuyerNote,
AdminNote: in.SubmitParam.AdminNote,
PreviousPackageCode: termination.PackageCode,
BusinessType: consts.PackageBusinessTypeChange,
OrderRemark: "sim card change order",
BindRemark: in.BatchNo,
Operator: in.Operator,
})
if err != nil {
return nil, err
}
if err = p.updateCardsAfterChange(ctx, tx, iccids, termination.CardsByIccid, subscribe, in.Operator.UserId); err != nil {
return nil, err
}
detailUpdates := make(map[string]any, len(iccids))
for _, row := range in.Rows {
card := termination.CardsByIccid[row.Iccid]
oldBind := termination.BindsByIccid[row.Iccid]
newBind := subscribe.BindsByIccid[row.Iccid]
detailUpdates[row.Iccid] = buildDetailSnapshot(row, card, oldBind, newBind, voidedByIccid[row.Iccid], in, termination, subscribe)
}
if err = updateBatchDetailSnapshots(ctx, tx, in.BatchNo, detailUpdates, in.Operator.UserId); err != nil {
return nil, err
}
if err = convertBatchDetailsAfterComplete(ctx, tx, in.BatchNo, in.Operator.UserId); err != nil {
return nil, err
}
output := &CompleteOutput{
BatchNo: in.BatchNo,
BatchType: consts.BatchTypeChange,
SourceBatchNo: sourceBatchNo,
SourceMode: sourceMode(sourceBatchNo),
Quantity: quantity,
CustomerAgentId: in.PriceSnapshot.CustomerAgentId,
CustomerName: "",
OldPackageCode: termination.PackageCode,
NewPackageCode: subscribe.Package.PackageCode,
NewPackageName: subscribe.Package.PackageName,
OrderSn: subscribe.OrderSn,
Amount: subscribe.Amount,
ChangeUnitPrice: in.PriceSnapshot.ChangeUnitPrice,
DurationMonths: in.SubmitParam.DurationMonths,
GiftDurationMonths: in.SubmitParam.GiftDurationMonths,
EffectiveMode: strings.TrimSpace(in.SubmitParam.EffectiveMode),
OfflineContractCompleted: in.SubmitParam.OfflineContractCompleted,
PackageBeginTime: subscribe.PackageBeginAt,
PackageEndTime: subscribe.PackageEndAt,
ChangeTime: subscribe.SubscribeTime,
}
if in.PrepareSnapshot != nil {
output.CustomerName = in.PrepareSnapshot.CustomerName
output.OldPackageName = in.PrepareSnapshot.OldPackageName
}
if err = updateBatchMainAfterComplete(ctx, tx, in, output); err != nil {
return nil, err
}
return output, nil
}
func (p *Processor) voidFuturePendingBinds(ctx context.Context, tx gdb.TX, iccids []string, batchNo string, voidTime *gtime.Time, userId int64) (map[string][]string, error) {
result := make(map[string][]string)
pendingBinds, err := p.repo.LockPendingBindsByIccids(ctx, tx, iccids)
if err != nil {
return nil, gerror.Wrap(err, "lock future pending binds failed")
}
if len(pendingBinds) == 0 {
return result, nil
}
orderCodeSet := make(map[string]struct{})
orderVoidCounts := make(map[string]int)
subscriptionNos := make([]string, 0, len(pendingBinds))
logRows := make([]do.TSimBindPackageLogs, 0, len(pendingBinds))
for _, bind := range pendingBinds {
if bind == nil {
continue
}
subscriptionNo := strings.TrimSpace(bind.SubscriptionNo)
if subscriptionNo == "" {
continue
}
subscriptionNos = append(subscriptionNos, subscriptionNo)
result[strings.TrimSpace(bind.Iccid)] = append(result[strings.TrimSpace(bind.Iccid)], subscriptionNo)
logRows = append(logRows, bindToVoidedHistory(bind, voidTime, batchNo, userId))
orderCode := strings.TrimSpace(bind.OrderCode)
if orderCode != "" {
orderCodeSet[orderCode] = struct{}{}
orderVoidCounts[orderCode]++
}
}
if len(logRows) > 0 {
if _, err = tx.Ctx(ctx).Model(dao.TSimBindPackageLogs.Table()).
Data(logRows).
OnConflict(dao.TSimBindPackageLogs.Columns().SubscriptionNo).
Save(); err != nil {
return nil, gerror.Wrap(err, "save voided bind history failed")
}
}
affected, err := p.repo.DeleteBindsBySubscriptionNos(ctx, tx, subscriptionNos)
if err != nil {
return nil, gerror.Wrap(err, "delete future pending binds failed")
}
if int(affected) != len(subscriptionNos) {
return nil, gerror.New("some future pending binds changed, please retry")
}
if err = p.updateOrdersAfterChangeVoid(ctx, tx, mapKeys(orderCodeSet), orderVoidCounts, batchNo, voidTime, userId); err != nil {
return nil, err
}
return result, nil
}
func bindToVoidedHistory(bind *entity.TSimCardBindPkg, actualExpireAt *gtime.Time, changeBatchNo string, userId int64) do.TSimBindPackageLogs {
return do.TSimBindPackageLogs{
SubscriptionNo: bind.SubscriptionNo,
BatchNo: bind.BatchNo,
Iccid: bind.Iccid,
Msisdn: bind.Msisdn,
DeptId: bind.DeptId,
PackageType: bind.PackageType,
PackageCode: bind.PackageCode,
PreviousPackageCode: bind.PreviousPackageCode,
BusinessType: bind.BusinessType,
EffectiveStatus: consts.EffectiveStatusVoided,
HandleTime: bind.HandleTime,
EffectiveTime: bind.EffectiveTime,
ExpectedExpireTime: bind.ExpectedExpireTime,
ActualExpireTime: actualExpireAt,
DurationMonths: bind.DurationMonths,
GiftDurationMonths: bind.GiftDurationMonths,
OrderCode: bind.OrderCode,
Remark: appendRemark(bind.Remark, fmt.Sprintf("changeBatchNo=%s", changeBatchNo)),
TenantId: bind.TenantId,
CreatedDept: bind.CreatedDept,
CreatedBy: bind.CreatedBy,
CreatedAt: bind.CreatedAt,
UpdatedBy: userId,
UpdatedAt: actualExpireAt,
}
}
func (p *Processor) updateOrdersAfterChangeVoid(ctx context.Context, tx gdb.TX, orderCodes []string, voidCounts map[string]int, batchNo string, voidTime *gtime.Time, userId int64) error {
if len(orderCodes) == 0 {
return nil
}
activeCounts, err := p.repo.ActiveBindCountsByOrderCodes(ctx, tx, orderCodes)
if err != nil {
return gerror.Wrap(err, "count active binds by order failed")
}
for _, orderCode := range orderCodes {
returnStatus := consts.OrderReturnStatusSuccess
if activeCounts[orderCode] > 0 {
returnStatus = consts.OrderReturnStatusPartial
}
if _, err = tx.Ctx(ctx).Model(dao.TOrder.Table()).
Where(dao.TOrder.Columns().OrderSn, orderCode).
Data(do.TOrder{
ReturnStatus: returnStatus,
UpdatedBy: userId,
UpdatedAt: voidTime,
}).
Update(); err != nil {
return gerror.Wrap(err, "update order return status failed")
}
if err = updateOrderItemChangeExtra(ctx, tx, orderCode, batchNo, voidCounts[orderCode], voidTime, userId); err != nil {
return err
}
}
return nil
}
func updateOrderItemChangeExtra(ctx context.Context, tx gdb.TX, orderCode, batchNo string, voidQuantity int, voidTime *gtime.Time, userId int64) error {
var item *entity.TOrderItem
if err := tx.Ctx(ctx).Model(dao.TOrderItem.Table()).
Where(dao.TOrderItem.Columns().OrderSn, orderCode).
Limit(1).
Scan(&item); err != nil {
return gerror.Wrap(err, "load order item failed")
}
if item == nil {
return nil
}
extra := g.Map{}
if strings.TrimSpace(item.ExtraSkuData) != "" {
_ = json.Unmarshal([]byte(item.ExtraSkuData), &extra)
}
extra["lastChangeBatchNo"] = batchNo
extra["lastChangeTime"] = formatTime(voidTime)
extra["voidedByChangeQuantity"] = voidQuantity
data, err := json.Marshal(extra)
if err != nil {
return gerror.Wrap(err, "marshal order item extra failed")
}
if _, err = tx.Ctx(ctx).Model(dao.TOrderItem.Table()).
Where(dao.TOrderItem.Columns().ItemId, item.ItemId).
Data(do.TOrderItem{
ExtraSkuData: string(data),
UpdatedBy: userId,
UpdatedAt: voidTime,
}).
Update(); err != nil {
return gerror.Wrap(err, "update order item extra failed")
}
return nil
}
func (p *Processor) updateCardsAfterChange(ctx context.Context, tx gdb.TX, iccids []string, cardsByIccid map[string]*entity.TSimCardInfo, subscribe *cardpackage.CreatePackageSubscriptionOutput, userId int64) error {
if subscribe == nil || subscribe.Package == nil {
return gerror.New("change subscription output is empty")
}
now := gtime.Now()
columns := dao.TSimCardInfo.Columns()
for _, iccid := range iccids {
card := cardsByIccid[iccid]
if card == nil {
return gerror.Newf("ICCID card info not found: %s", iccid)
}
result, err := tx.Ctx(ctx).Model(dao.TSimCardInfo.Table()).
Where(columns.Iccid, iccid).
Data(g.Map{
columns.CurrentPackageCode: subscribe.Package.PackageCode,
columns.PackageStatus: subscribe.PackageStatus,
columns.FlowPkgBeginAt: subscribe.PackageBeginAt,
columns.FlowPkgPresentAt: nil,
columns.FlowPkgRealPresentAt: nil,
columns.FlowPkgEndAt: subscribe.PackageEndAt,
columns.DataServiceStatus: 1,
columns.PackageTotalFlowMb: subscribe.Package.FlowMb,
columns.UsedFlowMb: 0,
columns.TrafficCutoffStatus: 0,
columns.TrafficCutoffThreshold: 0,
columns.SmsServiceStatus: 0,
columns.PackageTotalSms: 0,
columns.UsedSms: 0,
columns.VoiceServiceStatus: 0,
columns.PackageTotalVoice: 0,
columns.UsedVoice: 0,
columns.RoamServiceStatus: 0,
columns.PackageTotalRoamFlowMb: 0,
columns.UsedRoamFlowMb: 0,
columns.UpdatedBy: userId,
columns.UpdatedAt: now,
}).
Update()
if err != nil {
return gerror.Wrap(err, "update card after change failed")
}
affected, err := result.RowsAffected()
if err != nil {
return gerror.Wrap(err, "get updated card count failed")
}
if affected != 1 {
return gerror.Newf("ICCID card changed concurrently, retry: %s", iccid)
}
}
return nil
}
func updateBatchDetailSnapshots(ctx context.Context, tx gdb.TX, batchNo string, snapshots map[string]any, userId int64) error {
for iccid, snapshot := range snapshots {
_, err := tx.Ctx(ctx).Model(dao.BatchDetail.Table()).
Where(dao.BatchDetail.Columns().BatchNo, batchNo).
Where(dao.BatchDetail.Columns().ItemId, iccid).
Data(do.BatchDetail{
Snapshot: gjson.New(snapshot),
UpdatedBy: userId,
UpdatedAt: gtime.Now(),
}).
Update()
if err != nil {
return gerror.Wrap(err, "update change batch detail snapshot failed")
}
}
return nil
}
func convertBatchDetailsAfterComplete(ctx context.Context, tx gdb.TX, batchNo string, userId int64) error {
_, err := tx.Ctx(ctx).Model(dao.BatchDetail.Table()).
Where(dao.BatchDetail.Columns().BatchNo, batchNo).
Where(dao.BatchDetail.Columns().BatchType, consts.BatchTypePrepare).
Data(do.BatchDetail{
BatchType: consts.BatchTypeChange,
UpdatedBy: userId,
UpdatedAt: gtime.Now(),
}).
Update()
if err != nil {
return gerror.Wrap(err, "update change batch detail type failed")
}
return nil
}
func updateBatchMainAfterComplete(ctx context.Context, tx gdb.TX, in *CompleteInput, output *CompleteOutput) error {
info := g.Map{
"sourceMode": output.SourceMode,
"orderSn": output.OrderSn,
"customerAgentId": output.CustomerAgentId,
"customerName": output.CustomerName,
"oldPackageCode": output.OldPackageCode,
"oldPackageName": output.OldPackageName,
"newPackageCode": output.NewPackageCode,
"newPackageName": output.NewPackageName,
"durationMonths": output.DurationMonths,
"giftDurationMonths": output.GiftDurationMonths,
"effectiveMode": output.EffectiveMode,
"changeUnitPrice": output.ChangeUnitPrice,
"amount": output.Amount,
"quantity": output.Quantity,
"packageBeginTime": output.PackageBeginTime,
"packageEndTime": output.PackageEndTime,
"offlineContractCompleted": output.OfflineContractCompleted,
"changeTime": output.ChangeTime,
}
if output.SourceBatchNo != "" {
info["sourceBatchNo"] = output.SourceBatchNo
}
if in.PriceSnapshot != nil {
info["priceSource"] = in.PriceSnapshot.PriceSource
info["agentDefaultPriceId"] = in.PriceSnapshot.AgentDefaultPriceId
info["targetPriceId"] = in.PriceSnapshot.TargetPriceId
info["renewableDurationOptions"] = in.PriceSnapshot.RenewableDurationOptions
}
if buyerNote := strings.TrimSpace(in.SubmitParam.BuyerNote); buyerNote != "" {
info["buyerNote"] = buyerNote
}
if adminNote := strings.TrimSpace(in.SubmitParam.AdminNote); adminNote != "" {
info["adminNote"] = adminNote
}
if remark := strings.TrimSpace(in.SubmitParam.Remark); remark != "" {
info["remark"] = remark
}
_, err := tx.Ctx(ctx).Model(dao.BatchMain.Table()).
Where(dao.BatchMain.Columns().BatchNo, in.BatchNo).
Data(do.BatchMain{
BatchType: consts.BatchTypeChange,
OrderSn: output.OrderSn,
TargetDeptId: output.CustomerAgentId,
BatchInfo: gjson.New(info),
UpdatedBy: in.Operator.UserId,
UpdatedAt: output.ChangeTime,
}).
Update()
if err != nil {
return gerror.Wrap(err, "update change batch failed")
}
return nil
}
func buildDetailSnapshot(row *Row, card *entity.TSimCardInfo, oldBind, newBind *entity.TSimCardBindPkg, voidedSubscriptionNos []string, in *CompleteInput, termination *cardpackage.TerminateCurrentPackageOutput, subscribe *cardpackage.CreatePackageSubscriptionOutput) g.Map {
data := g.Map{
"ICCID": row.Iccid,
"MSISDN": row.Msisdn,
"IMSI": row.Imsi,
"sourceMode": sourceMode(in.SourceBatchNo),
"changeBatchNo": in.BatchNo,
"oldPackageCode": termination.PackageCode,
"newPackageCode": subscribe.Package.PackageCode,
"newPackageName": subscribe.Package.PackageName,
"voidedSubscriptionNos": voidedSubscriptionNos,
"durationMonths": in.SubmitParam.DurationMonths,
"giftDurationMonths": in.SubmitParam.GiftDurationMonths,
"effectiveMode": strings.TrimSpace(in.SubmitParam.EffectiveMode),
"offlineContractCompleted": in.SubmitParam.OfflineContractCompleted,
"changeUnitPrice": in.PriceSnapshot.ChangeUnitPrice,
"amount": roundAmount(in.PriceSnapshot.ChangeUnitPrice * float64(in.SubmitParam.DurationMonths)),
"newEffectiveTime": formatTime(subscribe.PackageBeginAt),
"newExpectedExpireTime": formatTime(subscribe.PackageEndAt),
"oldActualExpireTime": formatTime(termination.UnsubTime),
"rowNo": row.RowNo,
"rawFields": row.RawFields,
}
if strings.TrimSpace(in.SourceBatchNo) != "" {
data["sourceBatchNo"] = strings.TrimSpace(in.SourceBatchNo)
}
if card != nil {
data["ICCID"] = card.Iccid
data["MSISDN"] = card.Msisdn
data["IMSI"] = card.Imsi
}
if in.PrepareSnapshot != nil {
data["oldPackageName"] = in.PrepareSnapshot.OldPackageName
}
if oldBind != nil {
data["oldSubscriptionNo"] = oldBind.SubscriptionNo
data["oldOrderSn"] = oldBind.OrderCode
data["oldEffectiveTime"] = formatTime(oldBind.EffectiveTime)
data["oldExpectedExpireTime"] = formatTime(oldBind.ExpectedExpireTime)
}
if newBind != nil {
data["newSubscriptionNo"] = newBind.SubscriptionNo
data["newOrderSn"] = newBind.OrderCode
}
return data
}
func appendRemark(old, addition string) string {
old = strings.TrimSpace(old)
addition = strings.TrimSpace(addition)
if old == "" {
return addition
}
if addition == "" {
return old
}
return old + "; " + addition
}
func mapKeys(set map[string]struct{}) []string {
result := make([]string, 0, len(set))
for key := range set {
if strings.TrimSpace(key) != "" {
result = append(result, key)
}
}
return result
}
func roundAmount(value float64) float64 {
if value < 0 {
return 0
}
return float64(int(value*100+0.5)) / 100
}
func sourceMode(sourceBatchNo string) int {
if strings.TrimSpace(sourceBatchNo) != "" {
return consts.BatchSourceTypeBatchSelect
}
return 0
}