fa3d466579
- Added a new error for submission sync failures, improving error handling in the tender approval process. - Updated the `ToggleTenderApproval` method to handle the new error, providing clearer responses for sync issues. - Introduced a method to reopen terminal submissions when tender approvals are resubmitted, enhancing submission state management. - Implemented optimistic locking in the repository to prevent concurrent updates, improving data integrity. - Added unit tests for new submission handling logic, ensuring robust functionality and error management. This update strengthens the tender approval workflow and submission handling, ensuring better error reporting and state management.
169 lines
5.3 KiB
Go
169 lines
5.3 KiB
Go
package tender_submission
|
|
|
|
import (
|
|
"time"
|
|
"tm/pkg/mongo"
|
|
)
|
|
|
|
// SubmissionStatus represents the current step in the tender submission workflow.
|
|
type SubmissionStatus string
|
|
|
|
const (
|
|
StatusOpportunity SubmissionStatus = "opportunity"
|
|
StatusValidating SubmissionStatus = "validating"
|
|
StatusPartnerPending SubmissionStatus = "partner_pending"
|
|
StatusApplying SubmissionStatus = "applying"
|
|
StatusSentWaiting SubmissionStatus = "sent_waiting"
|
|
StatusRejected SubmissionStatus = "rejected"
|
|
StatusContract SubmissionStatus = "contract"
|
|
StatusNotApplied SubmissionStatus = "not_applied"
|
|
)
|
|
|
|
// SubmissionStage groups statuses into high-level workflow stages for UI display.
|
|
type SubmissionStage string
|
|
|
|
const (
|
|
StageOpportunity SubmissionStage = "opportunity"
|
|
StageInProgress SubmissionStage = "in_progress"
|
|
StageSubmitted SubmissionStage = "submitted"
|
|
StageNotApplied SubmissionStage = "not_applied"
|
|
)
|
|
|
|
// StatusHistory records a status change in the submission workflow.
|
|
type StatusHistory struct {
|
|
Status SubmissionStatus `bson:"status" json:"status"`
|
|
Reason string `bson:"reason,omitempty" json:"reason,omitempty"`
|
|
ChangedBy string `bson:"changed_by" json:"changed_by"`
|
|
ChangedAt int64 `bson:"changed_at" json:"changed_at"`
|
|
Description string `bson:"description,omitempty" json:"description,omitempty"`
|
|
}
|
|
|
|
// TenderSubmission tracks a company's tender submission workflow.
|
|
type TenderSubmission struct {
|
|
mongo.Model `bson:",inline"`
|
|
|
|
TenderID string `bson:"tender_id" json:"tender_id"`
|
|
CompanyID string `bson:"company_id" json:"company_id"`
|
|
CustomerID string `bson:"customer_id,omitempty" json:"customer_id,omitempty"`
|
|
Status SubmissionStatus `bson:"status" json:"status"`
|
|
Stage SubmissionStage `bson:"stage" json:"stage"`
|
|
History []StatusHistory `bson:"status_history" json:"status_history"`
|
|
}
|
|
|
|
func StageForStatus(status SubmissionStatus) SubmissionStage {
|
|
switch status {
|
|
case StatusOpportunity:
|
|
return StageOpportunity
|
|
case StatusValidating, StatusPartnerPending, StatusApplying:
|
|
return StageInProgress
|
|
case StatusSentWaiting, StatusRejected, StatusContract:
|
|
return StageSubmitted
|
|
case StatusNotApplied:
|
|
return StageNotApplied
|
|
default:
|
|
return StageOpportunity
|
|
}
|
|
}
|
|
|
|
func IsTerminalStatus(status SubmissionStatus) bool {
|
|
switch status {
|
|
case StatusRejected, StatusContract, StatusNotApplied:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func AllowedNextStatuses(current SubmissionStatus) []SubmissionStatus {
|
|
transitions := map[SubmissionStatus][]SubmissionStatus{
|
|
StatusOpportunity: {StatusValidating, StatusNotApplied},
|
|
StatusValidating: {StatusPartnerPending, StatusApplying, StatusNotApplied},
|
|
StatusPartnerPending: {StatusApplying, StatusNotApplied},
|
|
StatusApplying: {StatusSentWaiting, StatusNotApplied},
|
|
StatusSentWaiting: {StatusRejected, StatusContract},
|
|
StatusRejected: {},
|
|
StatusContract: {},
|
|
StatusNotApplied: {},
|
|
}
|
|
return transitions[current]
|
|
}
|
|
|
|
func (ts *TenderSubmission) GetID() string {
|
|
return ts.ID.Hex()
|
|
}
|
|
|
|
func (ts *TenderSubmission) SetCreatedAt(timestamp int64) {
|
|
ts.CreatedAt = timestamp
|
|
}
|
|
|
|
func (ts *TenderSubmission) SetUpdatedAt(timestamp int64) {
|
|
ts.UpdatedAt = timestamp
|
|
}
|
|
|
|
func (ts *TenderSubmission) AddStatusChange(status SubmissionStatus, reason, changedBy, description string) {
|
|
ts.History = append(ts.History, StatusHistory{
|
|
Status: status,
|
|
Reason: reason,
|
|
ChangedBy: changedBy,
|
|
ChangedAt: time.Now().Unix(),
|
|
Description: description,
|
|
})
|
|
ts.Status = status
|
|
ts.Stage = StageForStatus(status)
|
|
ts.SetUpdatedAt(time.Now().Unix())
|
|
}
|
|
|
|
// ReopenForApproval resets a terminal submission when the tender is approved again.
|
|
// This is only used by the approval sync path, not by user-initiated status updates.
|
|
func (ts *TenderSubmission) ReopenForApproval(changedBy, description string) {
|
|
ts.AddStatusChange(StatusOpportunity, "approval_resubmitted", changedBy, description)
|
|
}
|
|
|
|
func NewTenderSubmission(tenderID, companyID, customerID string) *TenderSubmission {
|
|
now := time.Now().Unix()
|
|
ts := &TenderSubmission{
|
|
TenderID: tenderID,
|
|
CompanyID: companyID,
|
|
CustomerID: customerID,
|
|
Status: StatusOpportunity,
|
|
Stage: StageOpportunity,
|
|
}
|
|
ts.SetCreatedAt(now)
|
|
ts.SetUpdatedAt(now)
|
|
ts.AddStatusChange(StatusOpportunity, "", customerID, "Submission workflow started")
|
|
return ts
|
|
}
|
|
|
|
func (ts *TenderSubmission) ToResponse() *TenderSubmissionResponse {
|
|
history := make([]StatusHistoryResponse, len(ts.History))
|
|
for i, h := range ts.History {
|
|
history[i] = StatusHistoryResponse{
|
|
Status: string(h.Status),
|
|
Reason: h.Reason,
|
|
ChangedBy: h.ChangedBy,
|
|
ChangedAt: h.ChangedAt,
|
|
Description: h.Description,
|
|
}
|
|
}
|
|
|
|
return &TenderSubmissionResponse{
|
|
ID: ts.GetID(),
|
|
TenderID: ts.TenderID,
|
|
CompanyID: ts.CompanyID,
|
|
CustomerID: ts.CustomerID,
|
|
Status: string(ts.Status),
|
|
Stage: string(ts.Stage),
|
|
History: history,
|
|
CreatedAt: ts.CreatedAt,
|
|
UpdatedAt: ts.UpdatedAt,
|
|
}
|
|
}
|
|
|
|
func (ts *TenderSubmission) ToResponseWithTender(tender *TenderDetails) *TenderSubmissionWithTenderResponse {
|
|
resp := ts.ToResponse()
|
|
return &TenderSubmissionWithTenderResponse{
|
|
TenderSubmissionResponse: *resp,
|
|
Tender: tender,
|
|
}
|
|
}
|