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 }