refactor platform domain states and permissions

This commit is contained in:
david
2026-07-29 14:25:38 +08:00
parent d4799cd320
commit 35a52c4b06
42 changed files with 507 additions and 364 deletions

View File

@@ -152,7 +152,7 @@ func CreateGasorderContract(ctx *gin.Context) {
return
}
contract := models.GasorderContract{
Entity: models.Entity{Identity: models.NewIdentity(), Status: common.StatusDraft},
Entity: models.Entity{Identity: models.NewIdentity(), Status: common.StatusEnable}, ContractStatus: common.StatusDraft,
ContractNo: request.ContractNo, UserAccountID: userID, GasBasicID: gasID, DeliveryBasicID: deliveryID,
Title: request.Title, Terms: request.Terms, FileURI: request.FileURI, DefaultDeliveryFee: request.DefaultDeliveryFee,
SignedAt: request.SignedAt, EffectiveAt: request.EffectiveAt, ExpiredAt: request.ExpiredAt,
@@ -192,7 +192,7 @@ func UpdateGasorderContract(ctx *gin.Context) {
return
}
result := impl.DBService.Model(&models.GasorderContract{}).
Where("identity = ? AND status = ?", ctx.Param("identity"), common.StatusDraft).
Where("identity = ? AND contract_status = ?", ctx.Param("identity"), common.StatusDraft).
Updates(map[string]any{"delivery_basic_id": deliveryID, "title": request.Title, "terms": request.Terms,
"file_uri": request.FileURI, "default_delivery_fee": request.DefaultDeliveryFee,
"signed_at": request.SignedAt, "effective_at": request.EffectiveAt, "expired_at": request.ExpiredAt})
@@ -228,13 +228,13 @@ func RenewGasorderContract(ctx *gin.Context) {
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("identity = ?", ctx.Param("identity")).First(&contract).Error; err != nil {
return err
}
if contract.Status != common.StatusActive && contract.Status != common.StatusExpired && contract.Status != common.StatusTerminated {
if contract.ContractStatus != common.StatusActive && contract.ContractStatus != common.StatusExpired && contract.ContractStatus != common.StatusTerminated {
return errors.New("contract cannot be renewed")
}
if err := tx.Model(&contract).Updates(map[string]any{"status": common.StatusActive, "effective_at": request.EffectiveAt, "expired_at": request.ExpiredAt}).Error; err != nil {
if err := tx.Model(&contract).Updates(map[string]any{"contract_status": common.StatusActive, "effective_at": request.EffectiveAt, "expired_at": request.ExpiredAt}).Error; err != nil {
return err
}
contract.Status, contract.EffectiveAt, contract.ExpiredAt = common.StatusActive, request.EffectiveAt, request.ExpiredAt
contract.ContractStatus, contract.EffectiveAt, contract.ExpiredAt = common.StatusActive, request.EffectiveAt, request.ExpiredAt
return tx.Create(contractRevision(contract, "renew", request.Reason, operatorIdentity, operatorName)).Error
})
if err != nil {
@@ -258,10 +258,10 @@ func changeGasorderContract(ctx *gin.Context, action string, target int) {
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("identity = ?", ctx.Param("identity")).First(&contract).Error; err != nil {
return err
}
if action == "activate" && contract.Status != common.StatusDraft && contract.Status != common.StatusTerminated {
if action == "activate" && contract.ContractStatus != common.StatusDraft && contract.ContractStatus != common.StatusTerminated {
return errors.New("contract cannot be activated")
}
if action == "terminate" && contract.Status != common.StatusActive {
if action == "terminate" && contract.ContractStatus != common.StatusActive {
return errors.New("contract cannot be terminated")
}
if target == common.StatusActive {
@@ -275,23 +275,23 @@ func changeGasorderContract(ctx *gin.Context, action string, target int) {
return errors.New("contract has no active product")
}
}
if err := tx.Model(&contract).Update("status", target).Error; err != nil {
if err := tx.Model(&contract).Update("contract_status", target).Error; err != nil {
return err
}
contract.Status = target
contract.ContractStatus = target
return tx.Create(contractRevision(contract, action, request.Reason, operatorIdentity, operatorName)).Error
})
if err != nil {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
infra.Response.Success(ctx, gin.H{"updated": true, "status": target})
infra.Response.Success(ctx, gin.H{"updated": true, "contract_status": target})
}
func contractRevision(contract models.GasorderContract, action, reason, operatorIdentity, operatorName string) *models.GasorderContractRevision {
return &models.GasorderContractRevision{
Entity: models.Entity{Identity: models.NewIdentity(), Status: common.StatusRecorded},
GasorderContractID: contract.ID, Action: action, ContractStatus: contract.Status,
Entity: models.Entity{Identity: models.NewIdentity(), Status: common.StatusEnable},
GasorderContractID: contract.ID, Action: action, ContractStatus: contract.ContractStatus,
EffectiveAt: contract.EffectiveAt, ExpiredAt: contract.ExpiredAt,
OperatorIdentity: operatorIdentity, OperatorName: operatorName, OccurredAt: time.Now(), Reason: reason,
}
@@ -312,13 +312,13 @@ func BindGasorderContractProduct(ctx *gin.Context) {
common.RespondRecordError(ctx, err)
return
}
if contract.Status != common.StatusDraft && contract.Status != common.StatusActive {
if contract.ContractStatus != common.StatusDraft && contract.ContractStatus != common.StatusActive {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
var product models.ProductInfo
if err := impl.DBService.Where("identity = ?", request.ProductIdentity).First(&product).Error; err != nil ||
!product.IsEnabled || product.Status == common.StatusScrapped || product.UserAccountID != contract.UserAccountID {
product.Status != common.StatusEnable || product.ProductStatus == common.StatusScrapped || product.UserAccountID != contract.UserAccountID {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
@@ -328,7 +328,7 @@ func BindGasorderContractProduct(ctx *gin.Context) {
return
}
binding := models.GasorderContractProduct{
Entity: models.Entity{Identity: models.NewIdentity(), Status: common.StatusBound},
Entity: models.Entity{Identity: models.NewIdentity(), Status: common.StatusEnable},
GasorderContractID: contract.ID, ProductInfoID: product.ID, ProductCode: product.Code,
ProductTypeName: productType.Name, ProductParams: product.Params, UnitPrice: request.UnitPrice, BoundAt: time.Now(),
}
@@ -350,6 +350,7 @@ func UnbindGasorderContractProduct(ctx *gin.Context) {
now := time.Now()
result := impl.DBService.Model(&models.GasorderContractProduct{}).
Where("identity = ? AND unbound_at IS NULL", ctx.Param("identity")).
Where("NOT EXISTS (SELECT 1 FROM gasorder_item WHERE gasorder_item.gasorder_contract_product_id = gasorder_contract_product.id AND gasorder_item.active = true)").
Updates(gasorderUnbindUpdates(request.Reason, now))
if result.Error != nil || result.RowsAffected != 1 {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
@@ -389,11 +390,11 @@ func CreateGasorderBasic(ctx *gin.Context) {
return err
}
var contract models.GasorderContract
if err := tx.Where("identity = ?", request.ContractIdentity).First(&contract).Error; err != nil {
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("identity = ?", request.ContractIdentity).First(&contract).Error; err != nil {
return err
}
now := time.Now()
if contract.Status != common.StatusActive || contract.EffectiveAt.After(now) || (contract.ExpiredAt != nil && !contract.ExpiredAt.After(now)) {
if contract.ContractStatus != common.StatusActive || contract.EffectiveAt.After(now) || (contract.ExpiredAt != nil && !contract.ExpiredAt.After(now)) {
return errors.New("contract is not active")
}
if request.CreatorType == "user" && creatorID != contract.UserAccountID {
@@ -417,8 +418,8 @@ func CreateGasorderBasic(ctx *gin.Context) {
var productAmount int64
for _, binding := range bindings {
var product models.ProductInfo
if err := tx.Where("id = ?", binding.ProductInfoID).First(&product).Error; err != nil ||
!product.IsEnabled || product.Status == common.StatusScrapped || product.UserAccountID != contract.UserAccountID {
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ?", binding.ProductInfoID).First(&product).Error; err != nil ||
product.Status != common.StatusEnable || product.ProductStatus == common.StatusScrapped || product.UserAccountID != contract.UserAccountID {
return errors.New("contract product is no longer eligible")
}
productAmount += binding.UnitPrice
@@ -428,7 +429,7 @@ func CreateGasorderBasic(ctx *gin.Context) {
return errors.New("invalid payable amount")
}
order = models.GasorderBasic{
Entity: models.Entity{Identity: models.NewIdentity(), Status: common.StatusCreated},
Entity: models.Entity{Identity: models.NewIdentity(), Status: common.StatusEnable}, OrderStatus: common.StatusCreated,
OrderNo: models.NewIdentity(), RequestNo: request.RequestNo, GasorderContractID: contract.ID,
UserAccountID: contract.UserAccountID, CreatorType: request.CreatorType, CreatorID: creatorID,
CreatorIdentity: request.CreatorIdentity, GasBasicID: contract.GasBasicID, DeliveryBasicID: contract.DeliveryBasicID,
@@ -443,7 +444,7 @@ func CreateGasorderBasic(ctx *gin.Context) {
}
for _, binding := range bindings {
item := models.GasorderItem{
Entity: models.Entity{Identity: models.NewIdentity(), Status: common.StatusOrdered},
Entity: models.Entity{Identity: models.NewIdentity(), Status: common.StatusEnable},
GasorderBasicID: order.ID, GasorderContractProductID: binding.ID, ProductInfoID: binding.ProductInfoID,
Active: true,
ProductCode: binding.ProductCode, ProductTypeName: binding.ProductTypeName,
@@ -493,20 +494,20 @@ func AssignGasorderBasic(ctx *gin.Context) {
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("identity = ?", ctx.Param("identity")).First(&order).Error; err != nil {
return err
}
if order.Status != common.StatusCreated && order.Status != common.StatusAssigned {
if order.OrderStatus != common.StatusCreated && order.OrderStatus != common.StatusAssigned {
return errors.New("order cannot be assigned")
}
if !validGasorderAssignment(order, delivery, staff) {
return errors.New("delivery or staff does not belong to the order organization")
}
previous := order.Status
previous := order.OrderStatus
if err := tx.Model(&order).Updates(map[string]any{
"delivery_basic_id": delivery.ID, "staff_account_id": staff.ID, "status": common.StatusAssigned,
"delivery_basic_id": delivery.ID, "staff_account_id": staff.ID, "order_status": common.StatusAssigned,
}).Error; err != nil {
return err
}
assignment := models.GasorderAssign{
Entity: models.Entity{Identity: models.NewIdentity(), Status: common.StatusRecorded},
Entity: models.Entity{Identity: models.NewIdentity(), Status: common.StatusEnable},
GasorderBasicID: order.ID, GasBasicID: order.GasBasicID, DeliveryBasicID: delivery.ID,
StaffAccountID: staff.ID, AssignerIdentity: operatorIdentity, AssignerName: operatorName,
AssignedAt: time.Now(), Reason: request.Reason,
@@ -523,7 +524,7 @@ func AssignGasorderBasic(ctx *gin.Context) {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
infra.Response.Success(ctx, gin.H{"updated": true, "status": common.StatusAssigned})
infra.Response.Success(ctx, gin.H{"updated": true, "order_status": common.StatusAssigned})
}
func GasorderStartFilling(ctx *gin.Context) {
@@ -561,14 +562,14 @@ func GasorderComplete(ctx *gin.Context) {
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("identity = ?", ctx.Param("identity")).First(&order).Error; err != nil {
return err
}
if order.Status != common.StatusAwaitingConfirmation {
if order.OrderStatus != common.StatusAwaitingConfirmation {
return errors.New("order cannot be completed")
}
if err := tx.Model(&order).Update("status", common.StatusCompleted).Error; err != nil {
if err := tx.Model(&order).Update("order_status", common.StatusCompleted).Error; err != nil {
return err
}
confirm := models.GasorderConfirm{
Entity: models.Entity{Identity: models.NewIdentity(), Status: common.StatusRecorded},
Entity: models.Entity{Identity: models.NewIdentity(), Status: common.StatusEnable},
GasorderBasicID: order.ID, ConfirmType: request.ConfirmType, RecipientName: request.RecipientName,
RecipientPhone: request.RecipientPhone, ProofURI: request.ProofURI, ConfirmedAt: time.Now(), Remark: request.Remark,
}
@@ -578,13 +579,13 @@ func GasorderComplete(ctx *gin.Context) {
if err := releaseGasorderProducts(tx, order.ID); err != nil {
return err
}
return tx.Create(gasorderStatusRecord(order.ID, order.Status, common.StatusCompleted, request.Reason, operatorIdentity, operatorName)).Error
return tx.Create(gasorderStatusRecord(order.ID, order.OrderStatus, common.StatusCompleted, request.Reason, operatorIdentity, operatorName)).Error
})
if err != nil {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
infra.Response.Success(ctx, gin.H{"updated": true, "status": common.StatusCompleted})
infra.Response.Success(ctx, gin.H{"updated": true, "order_status": common.StatusCompleted})
}
func GasorderException(ctx *gin.Context) {
@@ -605,11 +606,11 @@ func GasorderRecover(ctx *gin.Context) {
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("identity = ?", ctx.Param("identity")).First(&order).Error; err != nil {
return err
}
if order.Status != common.StatusException || order.PreviousStatus == common.StatusDraft {
if order.OrderStatus != common.StatusException || order.PreviousOrderStatus == common.StatusDraft {
return errors.New("order cannot recover")
}
target := order.PreviousStatus
if err := tx.Model(&order).Updates(map[string]any{"status": target, "previous_status": common.StatusDraft}).Error; err != nil {
target := order.PreviousOrderStatus
if err := tx.Model(&order).Updates(map[string]any{"order_status": target, "previous_order_status": common.StatusDraft}).Error; err != nil {
return err
}
return tx.Create(gasorderStatusRecord(order.ID, common.StatusException, target, request.Reason, operatorIdentity, operatorName)).Error
@@ -635,12 +636,12 @@ func transitionGasorder(ctx *gin.Context, target int) {
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("identity = ?", ctx.Param("identity")).First(&order).Error; err != nil {
return err
}
if !gasorderTransitionAllowed(order.Status, target) || (target == common.StatusFilling && order.DeliveryBasicID == 0) {
if !gasorderTransitionAllowed(order.OrderStatus, target) || (target == common.StatusFilling && order.DeliveryBasicID == 0) {
return errors.New("invalid order transition")
}
updates := map[string]any{"status": target}
updates := map[string]any{"order_status": target}
if target == common.StatusException {
updates["previous_status"] = order.Status
updates["previous_order_status"] = order.OrderStatus
}
if err := tx.Model(&order).Updates(updates).Error; err != nil {
return err
@@ -652,7 +653,7 @@ func transitionGasorder(ctx *gin.Context, target int) {
return err
}
track := models.GasorderTrack{
Entity: models.Entity{Identity: models.NewIdentity(), Status: common.StatusActive},
Entity: models.Entity{Identity: models.NewIdentity(), Status: common.StatusEnable},
GasorderBasicID: order.ID, StaffAccountID: order.StaffAccountID,
AttemptNo: attempt + 1, StartedAt: time.Now(),
}
@@ -674,13 +675,13 @@ func transitionGasorder(ctx *gin.Context, target int) {
return err
}
}
return tx.Create(gasorderStatusRecord(order.ID, order.Status, target, request.Reason, operatorIdentity, operatorName)).Error
return tx.Create(gasorderStatusRecord(order.ID, order.OrderStatus, target, request.Reason, operatorIdentity, operatorName)).Error
})
if err != nil {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
infra.Response.Success(ctx, gin.H{"updated": true, "status": target})
infra.Response.Success(ctx, gin.H{"updated": true, "order_status": target})
}
func gasorderTransitionAllowed(from, target int) bool {
@@ -734,7 +735,7 @@ func deliveryBelongsToGas(deliveryID, gasID uint64) bool {
func gasorderStatusRecord(orderID uint64, from, to int, reason, operatorIdentity, operatorName string) *models.GasorderStatus {
return &models.GasorderStatus{
Entity: models.Entity{Identity: models.NewIdentity(), Status: common.StatusRecorded},
Entity: models.Entity{Identity: models.NewIdentity(), Status: common.StatusEnable},
GasorderBasicID: orderID, FromStatus: from, ToStatus: to,
OperatorIdentity: operatorIdentity, OperatorName: operatorName, OccurredAt: time.Now(), Reason: reason,
}

View File

@@ -91,14 +91,14 @@ func TestGasorderStatusRecordIsImmutableSnapshot(t *testing.T) {
if record.GasorderBasicID != 7 || record.FromStatus != common.StatusAssigned || record.ToStatus != common.StatusFilling {
t.Fatalf("unexpected status record: %#v", record)
}
if record.Status != common.StatusRecorded || record.OccurredAt.IsZero() {
if record.Status != common.StatusEnable || record.OccurredAt.IsZero() {
t.Fatalf("status record lacks immutable metadata: %#v", record)
}
}
func TestContractRevisionKeepsSingleContractHistory(t *testing.T) {
contract := models.GasorderContract{
Entity: models.Entity{ID: 9, Status: common.StatusActive},
Entity: models.Entity{ID: 9, Status: common.StatusEnable}, ContractStatus: common.StatusActive,
}
revision := contractRevision(contract, "renew", "annual renewal", "operator-a", "Operator")
if revision.GasorderContractID != 9 || revision.Action != "renew" || revision.ContractStatus != common.StatusActive {