222 lines
4.8 KiB
Go
222 lines
4.8 KiB
Go
package domain
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
type TaskStatus string
|
|
|
|
const (
|
|
TaskStatusPending TaskStatus = "PENDING"
|
|
TaskStatusClaimed TaskStatus = "CLAIMED"
|
|
TaskStatusRunning TaskStatus = "RUNNING"
|
|
TaskStatusWaitingConfirmation TaskStatus = "WAITING_CONFIRMATION"
|
|
TaskStatusSucceeded TaskStatus = "SUCCEEDED"
|
|
TaskStatusFailed TaskStatus = "FAILED"
|
|
TaskStatusCanceled TaskStatus = "CANCELED"
|
|
|
|
CurrencyCNY = "CNY"
|
|
)
|
|
|
|
const (
|
|
MaxTitleRunes = 120
|
|
MaxTitleBytes = 2048
|
|
MaxSKUBytes = 512
|
|
MaxDescriptionBytes = 8192
|
|
MaxSourceRefBytes = 256
|
|
MaxCancelReasonBytes = 500
|
|
)
|
|
|
|
type PurchaseTask struct {
|
|
ID string
|
|
CreatorSubject string
|
|
SourceRef *string
|
|
Title string
|
|
Description string
|
|
SKU string
|
|
ImageAssetID string
|
|
Quantity int
|
|
MaxBudgetCents *int64
|
|
Currency string
|
|
Status TaskStatus
|
|
Version int64
|
|
CancelReason *string
|
|
CanceledAt *time.Time
|
|
CreatedAt time.Time
|
|
UpdatedAt time.Time
|
|
}
|
|
|
|
type TaskEvent struct {
|
|
ID string
|
|
TaskID string
|
|
Type string
|
|
Message string
|
|
OccurredAt time.Time
|
|
}
|
|
|
|
type TaskDetail struct {
|
|
Task PurchaseTask
|
|
Asset Asset
|
|
Events []TaskEvent
|
|
}
|
|
|
|
type TaskValidationError struct {
|
|
Fields map[string]string
|
|
}
|
|
|
|
func (e *TaskValidationError) Error() string {
|
|
return "purchase task validation failed"
|
|
}
|
|
|
|
func ValidateTaskInput(
|
|
creatorSubject string,
|
|
sourceRef *string,
|
|
title string,
|
|
description string,
|
|
sku string,
|
|
imageAssetID string,
|
|
quantity int,
|
|
) error {
|
|
fields := make(map[string]string)
|
|
if strings.TrimSpace(creatorSubject) == "" {
|
|
fields["creator_subject"] = "required"
|
|
}
|
|
if sourceRef != nil {
|
|
if strings.TrimSpace(*sourceRef) == "" {
|
|
fields["source_ref"] = "must not be blank"
|
|
} else if len([]byte(*sourceRef)) > MaxSourceRefBytes {
|
|
fields["source_ref"] = fmt.Sprintf(
|
|
"must not exceed %d UTF-8 bytes",
|
|
MaxSourceRefBytes,
|
|
)
|
|
}
|
|
}
|
|
if strings.TrimSpace(title) == "" {
|
|
fields["title"] = "required"
|
|
} else {
|
|
if utf8.RuneCountInString(title) > MaxTitleRunes {
|
|
fields["title"] = fmt.Sprintf(
|
|
"must not exceed %d characters",
|
|
MaxTitleRunes,
|
|
)
|
|
}
|
|
if len([]byte(title)) > MaxTitleBytes {
|
|
fields["title"] = fmt.Sprintf(
|
|
"must not exceed %d UTF-8 bytes",
|
|
MaxTitleBytes,
|
|
)
|
|
}
|
|
}
|
|
if len([]byte(description)) > MaxDescriptionBytes {
|
|
fields["description"] = fmt.Sprintf(
|
|
"must not exceed %d UTF-8 bytes",
|
|
MaxDescriptionBytes,
|
|
)
|
|
}
|
|
if strings.TrimSpace(sku) == "" {
|
|
fields["sku"] = "required"
|
|
} else if len([]byte(sku)) > MaxSKUBytes {
|
|
fields["sku"] = fmt.Sprintf(
|
|
"must not exceed %d UTF-8 bytes",
|
|
MaxSKUBytes,
|
|
)
|
|
}
|
|
if strings.TrimSpace(imageAssetID) == "" {
|
|
fields["image_asset_id"] = "required"
|
|
}
|
|
if quantity <= 0 {
|
|
fields["quantity"] = "must be a positive integer"
|
|
}
|
|
if len(fields) > 0 {
|
|
return &TaskValidationError{Fields: fields}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func ParseOptionalCNY(value *string) (*int64, error) {
|
|
if value == nil {
|
|
return nil, nil
|
|
}
|
|
trimmed := strings.TrimSpace(*value)
|
|
if trimmed == "" {
|
|
return nil, errors.New("budget must be omitted or positive")
|
|
}
|
|
if strings.HasPrefix(trimmed, "+") || strings.HasPrefix(trimmed, "-") {
|
|
return nil, errors.New("budget must be positive")
|
|
}
|
|
parts := strings.Split(trimmed, ".")
|
|
if len(parts) > 2 || parts[0] == "" || len(parts[0]) > 16 {
|
|
return nil, errors.New("budget format is invalid")
|
|
}
|
|
if !allDigits(parts[0]) {
|
|
return nil, errors.New("budget format is invalid")
|
|
}
|
|
fraction := ""
|
|
if len(parts) == 2 {
|
|
fraction = parts[1]
|
|
if fraction == "" || len(fraction) > 2 || !allDigits(fraction) {
|
|
return nil, errors.New("budget format is invalid")
|
|
}
|
|
}
|
|
for len(fraction) < 2 {
|
|
fraction += "0"
|
|
}
|
|
whole, err := strconv.ParseInt(parts[0], 10, 64)
|
|
if err != nil || whole > (int64(^uint64(0)>>1)-99)/100 {
|
|
return nil, errors.New("budget is too large")
|
|
}
|
|
centsPart, err := strconv.ParseInt(fraction, 10, 64)
|
|
if err != nil {
|
|
return nil, errors.New("budget format is invalid")
|
|
}
|
|
cents := whole*100 + centsPart
|
|
if cents <= 0 {
|
|
return nil, errors.New("budget must be positive")
|
|
}
|
|
return ¢s, nil
|
|
}
|
|
|
|
func FormatOptionalCNY(cents *int64) *string {
|
|
if cents == nil {
|
|
return nil
|
|
}
|
|
value := fmt.Sprintf("%d.%02d", *cents/100, *cents%100)
|
|
return &value
|
|
}
|
|
|
|
func CanCancel(status TaskStatus) bool {
|
|
return status == TaskStatusPending
|
|
}
|
|
|
|
func IsValidTaskStatus(status TaskStatus) bool {
|
|
switch status {
|
|
case TaskStatusPending,
|
|
TaskStatusClaimed,
|
|
TaskStatusRunning,
|
|
TaskStatusWaitingConfirmation,
|
|
TaskStatusSucceeded,
|
|
TaskStatusFailed,
|
|
TaskStatusCanceled:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func allDigits(value string) bool {
|
|
if value == "" {
|
|
return false
|
|
}
|
|
for _, char := range value {
|
|
if char < '0' || char > '9' {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|