package renewchange import ( "context" "encoding/json" "sort" "strings" "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" "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/consts" "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model/do" ) // ChangeBindHistoryDO is the repository-local shape of the history table. // It deliberately lives outside the generated model because Worker does not // generate the card-package domain tables. type ChangeBindHistoryDO struct { g.Meta `orm:"table:t_sim_bind_package_logs, do:true"` SubscriptionNo any `orm:"subscription_no"` BatchNo any `orm:"batch_no"` Iccid any `orm:"iccid"` Msisdn any `orm:"msisdn"` DeptId any `orm:"dept_id"` PackageType any `orm:"package_type"` PackageCode any `orm:"package_code"` PreviousPackageCode any `orm:"previous_package_code"` BusinessType any `orm:"business_type"` EffectiveStatus any `orm:"effective_status"` HandleTime *gtime.Time `orm:"handle_time"` EffectiveTime *gtime.Time `orm:"effective_time"` ExpectedExpireTime *gtime.Time `orm:"expected_expire_time"` ActualExpireTime *gtime.Time `orm:"actual_expire_time"` DurationMonths any `orm:"duration_months"` GiftDurationMonths any `orm:"gift_duration_months"` OrderCode any `orm:"order_code"` Remark any `orm:"remark"` TenantId any `orm:"tenant_id"` CreatedDept any `orm:"created_dept"` CreatedBy any `orm:"created_by"` CreatedAt *gtime.Time `orm:"created_at"` UpdatedBy any `orm:"updated_by"` UpdatedAt *gtime.Time `orm:"updated_at"` } type changeOrderUpdateDO struct { g.Meta `orm:"table:t_order, do:true"` ReturnStatus any `orm:"return_status"` UpdatedBy any `orm:"updated_by"` UpdatedAt *gtime.Time `orm:"updated_at"` } type changeOrderItemRow struct { ItemId int64 `orm:"item_id"` ExtraSkuData string `orm:"extra_sku_data"` } type changeOrderItemUpdateDO struct { g.Meta `orm:"table:t_order_item, do:true"` ExtraSkuData any `orm:"extra_sku_data"` UpdatedBy any `orm:"updated_by"` UpdatedAt *gtime.Time `orm:"updated_at"` } type changeCardUpdateDO struct { g.Meta `orm:"table:t_sim_card_info, do:true"` CurrentPackageCode any `orm:"current_package_code"` PackageStatus any `orm:"package_status"` FlowPkgBeginAt any `orm:"flow_pkg_begin_at"` FlowPkgPresentAt any `orm:"flow_pkg_present_at"` FlowPkgRealPresentAt any `orm:"flow_pkg_real_present_at"` FlowPkgEndAt any `orm:"flow_pkg_end_at"` DataServiceStatus any `orm:"data_service_status"` PackageTotalFlowMb any `orm:"package_total_flow_mb"` UsedFlowMb any `orm:"used_flow_mb"` TrafficCutoffStatus any `orm:"traffic_cutoff_status"` TrafficCutoffThreshold any `orm:"traffic_cutoff_threshold"` SmsServiceStatus any `orm:"sms_service_status"` PackageTotalSms any `orm:"package_total_sms"` UsedSms any `orm:"used_sms"` VoiceServiceStatus any `orm:"voice_service_status"` PackageTotalVoice any `orm:"package_total_voice"` UsedVoice any `orm:"used_voice"` RoamServiceStatus any `orm:"roam_service_status"` PackageTotalRoamFlowMb any `orm:"package_total_roam_flow_mb"` UsedRoamFlowMb any `orm:"used_roam_flow_mb"` UpdatedBy any `orm:"updated_by"` UpdatedAt *gtime.Time `orm:"updated_at"` } type ChangeDeptRow struct { DeptId int64 `orm:"dept_id"` ParentId int64 `orm:"parent_id"` TenantId string `orm:"tenant_id"` } // LockChangeBatch locks the formal change batch so a duplicate delivery // cannot create a second order after the first transaction commits. func (r *Repository) LockChangeBatch(ctx context.Context, tx gdb.TX, batchNo string) (*BatchMainRow, error) { var main BatchMainRow if err := tx.Ctx(ctx).Model(TableBatchMain). Where("batch_no", strings.TrimSpace(batchNo)). Where("batch_type", consts.BatchTypeChange). LockUpdate(). Scan(&main); err != nil { return nil, gerror.Wrap(err, "lock change batch failed") } if main.Id == 0 { return nil, gerror.New("change batch does not exist") } return &main, nil } // LockChangeDetails locks all details, allowing the executor to verify that // the claimed batch still has exactly the expected running rows. func (r *Repository) LockChangeDetails(ctx context.Context, tx gdb.TX, batchNo string) ([]*BatchDetailRow, error) { var rows []*BatchDetailRow if err := tx.Ctx(ctx).Model(TableBatchDetail). Where("batch_no", strings.TrimSpace(batchNo)). Where("batch_type", consts.BatchTypeChange). OrderAsc("row_no"). LockUpdate(). Scan(&rows); err != nil { return nil, gerror.Wrap(err, "lock change details failed") } return rows, nil } // LockChangeBinds returns every pending/effective bind for the requested // cards. The service chooses the single current bind and treats equal-priority // duplicates as an integrity error. func (r *Repository) LockChangeBinds(ctx context.Context, tx gdb.TX, iccids []string) ([]*SimCardBindPkg, error) { return r.LockActiveBindRowsByIccids(ctx, tx, iccids) } func (r *Repository) LoadChangePackage(ctx context.Context, tx gdb.TX, tenantID, packageCode string) (*FlowPackageInfo, error) { var row FlowPackageInfo if err := tx.Ctx(ctx).Model(TableSimFlowPackageInfo). Where("tenant_id", strings.TrimSpace(tenantID)). Where("package_code", strings.TrimSpace(packageCode)). LockUpdate(). Scan(&row); err != nil { return nil, gerror.Wrap(err, "load change package failed") } if row.Id == 0 { return nil, gerror.Newf("package not found: %s", packageCode) } return &row, nil } func (r *Repository) LoadChangeDept(ctx context.Context, tx gdb.TX, tenantID string, deptID int64) (*ChangeDeptRow, error) { var row ChangeDeptRow if err := tx.Ctx(ctx).Model(TableSysDept). Where("tenant_id", strings.TrimSpace(tenantID)). Where("dept_id", deptID). Scan(&row); err != nil { return nil, gerror.Wrap(err, "load change customer failed") } if row.DeptId == 0 { return nil, gerror.New("change customer does not exist") } return &row, nil } func (r *Repository) ArchiveChangeBinds(ctx context.Context, tx gdb.TX, binds []*SimCardBindPkg, status int, actualExpireAt *gtime.Time, changeBatchNo string, userID int64) error { if len(binds) == 0 { return nil } rows := make([]ChangeBindHistoryDO, 0, len(binds)) for _, bind := range binds { if bind == nil || strings.TrimSpace(bind.SubscriptionNo) == "" { return gerror.New("change subscription history is incomplete") } rows = append(rows, ChangeBindHistoryDO{ 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: status, HandleTime: bind.HandleTime, EffectiveTime: bind.EffectiveTime, ExpectedExpireTime: bind.ExpectedExpireTime, ActualExpireTime: actualExpireAt, DurationMonths: bind.DurationMonths, GiftDurationMonths: bind.GiftDurationMonths, OrderCode: bind.OrderCode, Remark: appendChangeRemark(bind.Remark, changeBatchNo), TenantId: bind.TenantId, CreatedDept: bind.CreatedDept, CreatedBy: bind.CreatedBy, CreatedAt: bind.CreatedAt, UpdatedBy: userID, UpdatedAt: actualExpireAt, }) } if _, err := tx.Ctx(ctx).Model(TableSimBindPackageLogs). Data(rows). OnConflict("subscription_no"). Save(); err != nil { return gerror.Wrap(err, "archive change subscriptions failed") } return nil } func (r *Repository) DeleteChangeBinds(ctx context.Context, tx gdb.TX, subscriptionNos []string) (int64, error) { unique := uniqueStrings(subscriptionNos) if len(unique) == 0 { return 0, nil } result, err := tx.Ctx(ctx).Model(TableSimCardBindPkg). WhereIn("subscription_no", unique). Delete() if err != nil { return 0, gerror.Wrap(err, "delete change subscriptions failed") } affected, err := result.RowsAffected() if err != nil { return 0, gerror.Wrap(err, "read deleted change subscriptions count failed") } return affected, nil } func (r *Repository) UpdateChangeOrdersAfterRemoval(ctx context.Context, tx gdb.TX, orderCodes []string, batchNo, batchKey string, quantityByOrder map[string]int, eventTime *gtime.Time, userID int64) error { orderCodes = uniqueStrings(orderCodes) sort.Strings(orderCodes) if len(orderCodes) == 0 { return nil } if eventTime == nil { eventTime = gtime.Now() } activeCounts, err := countChangeBindsByOrder(ctx, tx, orderCodes) if err != nil { return err } for _, orderCode := range orderCodes { returnStatus := consts.OrderReturnStatusSuccess if activeCounts[orderCode] > 0 { returnStatus = consts.OrderReturnStatusPartial } result, updateErr := tx.Ctx(ctx).Model(TableOrder). Where("order_sn", orderCode). Data(buildChangeOrderUpdateDO(returnStatus, userID, eventTime)). Update() if updateErr != nil { return gerror.Wrap(updateErr, "update changed order return status failed") } affected, affectedErr := result.RowsAffected() if affectedErr != nil { return gerror.Wrap(affectedErr, "read changed order update count failed") } if affected != 1 { return gerror.Newf("changed source order does not exist: %s", orderCode) } if err = r.updateChangeOrderItemExtra(ctx, tx, orderCode, batchNo, batchKey, quantityByOrder[orderCode], eventTime, userID); err != nil { return err } } return nil } func (r *Repository) updateChangeOrderItemExtra(ctx context.Context, tx gdb.TX, orderCode, batchNo, batchKey string, quantity int, eventTime *gtime.Time, userID int64) error { var item changeOrderItemRow if err := tx.Ctx(ctx).Model(TableOrderItem). Where("order_sn", orderCode). Limit(1). Scan(&item); err != nil { return gerror.Wrap(err, "load changed order item failed") } if item.ItemId == 0 { return gerror.Newf("changed order item does not exist: %s", orderCode) } extra := map[string]any{} if strings.TrimSpace(item.ExtraSkuData) != "" { if err := json.Unmarshal([]byte(item.ExtraSkuData), &extra); err != nil { return gerror.Wrap(err, "parse changed order item extra failed") } } extra[batchKey+"BatchNo"] = batchNo extra[batchKey+"Time"] = eventTime.Format("Y-m-d H:i:s") if batchKey == "lastUnsubscribe" { extra["unsubscribedQuantity"] = quantity } else { extra["voidedByChangeQuantity"] = quantity } data, err := json.Marshal(extra) if err != nil { return gerror.Wrap(err, "marshal changed order item extra failed") } _, err = tx.Ctx(ctx).Model(TableOrderItem). Where("item_id", item.ItemId). Data(buildChangeOrderItemUpdateDO(string(data), userID, eventTime)). Update() return gerror.Wrap(err, "update changed order item extra failed") } func buildChangeOrderUpdateDO(returnStatus int, userID int64, updatedAt *gtime.Time) changeOrderUpdateDO { return changeOrderUpdateDO{ReturnStatus: returnStatus, UpdatedBy: userID, UpdatedAt: updatedAt} } func buildChangeOrderItemUpdateDO(extraSKUData string, userID int64, updatedAt *gtime.Time) changeOrderItemUpdateDO { return changeOrderItemUpdateDO{ExtraSkuData: extraSKUData, UpdatedBy: userID, UpdatedAt: updatedAt} } func countChangeBindsByOrder(ctx context.Context, tx gdb.TX, orderCodes []string) (map[string]int, error) { type orderCountRow struct { OrderCode string `orm:"order_code"` Count int `orm:"count"` } var rows []orderCountRow if err := tx.Ctx(ctx).Model(TableSimCardBindPkg). Fields("order_code", "COUNT(1) AS count"). WhereIn("order_code", orderCodes). Group("order_code"). Scan(&rows); err != nil { return nil, gerror.Wrap(err, "count changed order subscriptions failed") } counts := make(map[string]int, len(rows)) for _, row := range rows { counts[strings.TrimSpace(row.OrderCode)] = row.Count } return counts, nil } func (r *Repository) UpdateChangeCard(ctx context.Context, tx gdb.TX, iccid, packageCode string, packageStatus int, packageFlowMb float64, beginAt, endAt *gtime.Time, userID int64) error { result, err := tx.Ctx(ctx).Model(TableSimCardInfo). Where("iccid", strings.TrimSpace(iccid)). Data(changeCardUpdateDO{ CurrentPackageCode: packageCode, PackageStatus: packageStatus, FlowPkgBeginAt: beginAt, FlowPkgPresentAt: gdb.Raw("NULL"), FlowPkgRealPresentAt: gdb.Raw("NULL"), FlowPkgEndAt: endAt, DataServiceStatus: 1, PackageTotalFlowMb: packageFlowMb, UsedFlowMb: 0, TrafficCutoffStatus: 0, TrafficCutoffThreshold: 0, SmsServiceStatus: 0, PackageTotalSms: 0, UsedSms: 0, VoiceServiceStatus: 0, PackageTotalVoice: 0, UsedVoice: 0, RoamServiceStatus: 0, PackageTotalRoamFlowMb: 0, UsedRoamFlowMb: 0, UpdatedBy: userID, UpdatedAt: gtime.Now(), }). Update() if err != nil { return gerror.Wrap(err, "update changed card package failed") } affected, err := result.RowsAffected() if err != nil { return gerror.Wrap(err, "read changed card update count failed") } if affected != 1 { return gerror.Newf("changed card was modified concurrently: %s", iccid) } return nil } func (r *Repository) MarkChangeDetailsSuccess(ctx context.Context, tx gdb.TX, batchNo string, details []*BatchDetailRow, snapshots map[int]*gjson.Json, userID int64) error { for _, detail := range details { if detail == nil { continue } data := do.BatchDetail{ DetailStatus: consts.BatchDetailStatusSuccess, ErrorCode: "", ErrorReason: "", Snapshot: snapshots[detail.RowNo], UpdatedBy: userID, } result, err := tx.Ctx(ctx).Model(TableBatchDetail). Where("batch_no", batchNo). Where("batch_type", consts.BatchTypeChange). Where("row_no", detail.RowNo). Data(data). Update() if err != nil { return gerror.Wrap(err, "mark change detail success failed") } affected, affectedErr := result.RowsAffected() if affectedErr != nil || affected != 1 { return gerror.Newf("change detail disappeared during success update: row %d", detail.RowNo) } } return nil } func appendChangeRemark(old, batchNo string) string { addition := "changeBatchNo=" + strings.TrimSpace(batchNo) old = strings.TrimSpace(old) if old == "" { return addition } return old + "; " + addition }