144 lines
4.9 KiB
Go
144 lines
4.9 KiB
Go
package auth
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"strings"
|
|
"time"
|
|
|
|
"gorm.io/gorm"
|
|
|
|
"wucher/internal/shared/pkg/uuidv7"
|
|
"wucher/internal/queue"
|
|
)
|
|
|
|
const (
|
|
EmailOutboxStatusPending = "pending"
|
|
EmailOutboxStatusProcessing = "processing"
|
|
EmailOutboxStatusPublished = "published"
|
|
EmailOutboxStatusDead = "dead"
|
|
)
|
|
|
|
type EmailOutboxMessage struct {
|
|
ID []byte `gorm:"type:binary(16);primaryKey;column:id"`
|
|
MessageID string `gorm:"type:varchar(64);uniqueIndex;not null;column:message_id"`
|
|
Kind string `gorm:"type:varchar(64);index;not null;column:kind"`
|
|
JobType string `gorm:"type:varchar(64);index;not null;column:job_type"`
|
|
Body []byte `gorm:"type:longblob;not null;column:body"`
|
|
Attributes []byte `gorm:"type:longblob;column:attributes"`
|
|
MessageGroupID string `gorm:"type:varchar(128);column:message_group_id"`
|
|
DeduplicationID string `gorm:"type:varchar(128);column:deduplication_id"`
|
|
Status string `gorm:"type:varchar(32);index;not null;column:status"`
|
|
Attempts int `gorm:"not null;default:0;column:attempts"`
|
|
AvailableAt time.Time `gorm:"index;not null;column:available_at"`
|
|
LockedAt *time.Time `gorm:"index;column:locked_at"`
|
|
LockedBy string `gorm:"type:varchar(64);column:locked_by"`
|
|
PublishedAt *time.Time `gorm:"column:published_at"`
|
|
LastError string `gorm:"type:text;column:last_error"`
|
|
CorrelationID string `gorm:"type:varchar(64);index;column:correlation_id"`
|
|
TraceID string `gorm:"type:varchar(64);index;column:trace_id"`
|
|
CreatedAt time.Time
|
|
UpdatedAt time.Time
|
|
}
|
|
|
|
func (EmailOutboxMessage) TableName() string { return "email_outbox_messages" }
|
|
|
|
func (m *EmailOutboxMessage) BeforeCreate(_ *gorm.DB) error {
|
|
if len(m.ID) == 0 {
|
|
m.ID = uuidv7.MustBytes()
|
|
}
|
|
if strings.TrimSpace(m.Status) == "" {
|
|
m.Status = EmailOutboxStatusPending
|
|
}
|
|
if m.AvailableAt.IsZero() {
|
|
m.AvailableAt = time.Now().UTC()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func NewEmailOutboxMessage(now time.Time, message *queue.OutboundMessage) (*EmailOutboxMessage, error) {
|
|
if message == nil {
|
|
return nil, errors.New("outbound message is required")
|
|
}
|
|
if strings.TrimSpace(message.ID) == "" {
|
|
return nil, errors.New("outbound message id is required")
|
|
}
|
|
if len(message.Body) == 0 {
|
|
return nil, errors.New("outbound message body is required")
|
|
}
|
|
|
|
attributes, err := json.Marshal(message.Attributes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if now.IsZero() {
|
|
now = time.Now().UTC()
|
|
}
|
|
|
|
kind := strings.TrimSpace(message.Attributes["message_kind"])
|
|
if kind == "" {
|
|
kind = queue.EmailKind
|
|
}
|
|
jobType := strings.TrimSpace(message.Attributes["job_type"])
|
|
if jobType == "" {
|
|
jobType = "generic"
|
|
}
|
|
|
|
return &EmailOutboxMessage{
|
|
MessageID: strings.TrimSpace(message.ID),
|
|
Kind: kind,
|
|
JobType: jobType,
|
|
Body: append([]byte(nil), message.Body...),
|
|
Attributes: append([]byte(nil), attributes...),
|
|
MessageGroupID: strings.TrimSpace(message.MessageGroupID),
|
|
DeduplicationID: strings.TrimSpace(message.DeduplicationID),
|
|
Status: EmailOutboxStatusPending,
|
|
AvailableAt: now.UTC(),
|
|
CorrelationID: strings.TrimSpace(message.Attributes["correlation_id"]),
|
|
TraceID: strings.TrimSpace(message.Attributes["trace_id"]),
|
|
}, nil
|
|
}
|
|
|
|
func (m *EmailOutboxMessage) OutboundMessage() (*queue.OutboundMessage, error) {
|
|
if m == nil {
|
|
return nil, errors.New("email outbox message is required")
|
|
}
|
|
attributes := map[string]string{}
|
|
if len(m.Attributes) > 0 {
|
|
if err := json.Unmarshal(m.Attributes, &attributes); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if strings.TrimSpace(m.MessageID) == "" {
|
|
return nil, errors.New("message id is required")
|
|
}
|
|
if len(m.Body) == 0 {
|
|
return nil, errors.New("message body is required")
|
|
}
|
|
return &queue.OutboundMessage{
|
|
ID: m.MessageID,
|
|
Body: append([]byte(nil), m.Body...),
|
|
Attributes: attributes,
|
|
MessageGroupID: strings.TrimSpace(m.MessageGroupID),
|
|
DeduplicationID: strings.TrimSpace(m.DeduplicationID),
|
|
}, nil
|
|
}
|
|
|
|
type EmailOutboxWriter interface {
|
|
CreateEmailOutboxMessage(ctx context.Context, message *EmailOutboxMessage) error
|
|
}
|
|
|
|
type EmailOutboxRepository interface {
|
|
EmailOutboxWriter
|
|
ClaimPendingEmailOutboxMessages(ctx context.Context, limit int, now time.Time, lockTTL time.Duration, workerID string) ([]EmailOutboxMessage, error)
|
|
MarkEmailOutboxMessagePublished(ctx context.Context, id []byte, publishedAt time.Time) error
|
|
MarkEmailOutboxMessageRetry(ctx context.Context, id []byte, availableAt time.Time, lastErr string) error
|
|
MarkEmailOutboxMessageDead(ctx context.Context, id []byte, failedAt time.Time, lastErr string) error
|
|
CountPendingEmailOutboxMessages(ctx context.Context, now time.Time) (int64, error)
|
|
}
|
|
|
|
type TransactionalRepository interface {
|
|
WithTransaction(ctx context.Context, fn func(repo Repository, outbox EmailOutboxWriter) error) error
|
|
}
|