package repository import ( "baron-sso-backend/internal/domain" "context" "time" "gorm.io/gorm" "gorm.io/gorm/clause" ) type WorksmobileOutboxRepository interface { Create(ctx context.Context, item *domain.WorksmobileOutbox) error ListRecent(ctx context.Context, limit int) ([]domain.WorksmobileOutbox, error) ListCredentialBatchJobs(ctx context.Context, tenantRootID, credentialBatchID string) ([]domain.WorksmobileOutbox, error) UpdatePayload(ctx context.Context, id string, payload domain.JSONMap) error DeletePendingByTenantRoot(ctx context.Context, tenantRootID string) (int64, error) ListReady(ctx context.Context, limit int) ([]domain.WorksmobileOutbox, error) FindByID(ctx context.Context, id string) (*domain.WorksmobileOutbox, error) MarkRetry(ctx context.Context, id string) error MarkProcessing(ctx context.Context, id string) (bool, error) MarkProcessed(ctx context.Context, id string) error MarkFailed(ctx context.Context, id string, message string, nextAttemptAt time.Time) error } type worksmobileOutboxRepository struct { db *gorm.DB } func NewWorksmobileOutboxRepository(db *gorm.DB) WorksmobileOutboxRepository { return &worksmobileOutboxRepository{db: db} } func (r *worksmobileOutboxRepository) Create(ctx context.Context, item *domain.WorksmobileOutbox) error { if item.Payload == nil { item.Payload = domain.JSONMap{} } if item.Status == "" { item.Status = domain.WorksmobileOutboxStatusPending } return r.db.WithContext(ctx).Clauses(clause.OnConflict{ Columns: []clause.Column{{Name: "dedupe_key"}}, DoUpdates: clause.Assignments(map[string]any{ "payload": item.Payload, "status": domain.WorksmobileOutboxStatusPending, "last_error": "", "next_attempt_at": nil, "updated_at": time.Now(), }), }).Create(item).Error } func (r *worksmobileOutboxRepository) ListRecent(ctx context.Context, limit int) ([]domain.WorksmobileOutbox, error) { if limit <= 0 || limit > 1000 { limit = 50 } var rows []domain.WorksmobileOutbox err := r.db.WithContext(ctx).Order("created_at desc").Limit(limit).Find(&rows).Error return rows, err } func (r *worksmobileOutboxRepository) ListCredentialBatchJobs(ctx context.Context, tenantRootID, credentialBatchID string) ([]domain.WorksmobileOutbox, error) { query := r.db.WithContext(ctx). Where("resource_type = ? AND payload ->> 'tenantRootId' = ? AND coalesce(payload ->> 'credentialBatchId', '') <> ?", domain.WorksmobileResourceUser, tenantRootID, "") if credentialBatchID != "" { query = query.Where("payload ->> 'credentialBatchId' = ?", credentialBatchID) } var rows []domain.WorksmobileOutbox err := query.Order("created_at desc").Find(&rows).Error return rows, err } func (r *worksmobileOutboxRepository) UpdatePayload(ctx context.Context, id string, payload domain.JSONMap) error { return r.db.WithContext(ctx).Model(&domain.WorksmobileOutbox{}).Where("id = ?", id).Updates(map[string]any{ "payload": payload, "updated_at": time.Now(), }).Error } func (r *worksmobileOutboxRepository) DeletePendingByTenantRoot(ctx context.Context, tenantRootID string) (int64, error) { result := r.db.WithContext(ctx). Where("status = ? AND payload ->> 'tenantRootId' = ?", domain.WorksmobileOutboxStatusPending, tenantRootID). Delete(&domain.WorksmobileOutbox{}) return result.RowsAffected, result.Error } func (r *worksmobileOutboxRepository) ListReady(ctx context.Context, limit int) ([]domain.WorksmobileOutbox, error) { if limit <= 0 || limit > 100 { limit = 20 } var rows []domain.WorksmobileOutbox err := r.db.WithContext(ctx).Raw(` WITH RECURSIVE candidates AS ( SELECT *, NULLIF(payload #>> '{request,orgUnitExternalKey}', '') AS org_external_key, CASE WHEN payload #>> '{request,parentOrgUnitId}' LIKE 'externalKey:%' THEN NULLIF(substr(payload #>> '{request,parentOrgUnitId}', length('externalKey:') + 1), '') ELSE '' END AS parent_external_key FROM worksmobile_outboxes WHERE status = ? AND (next_attempt_at IS NULL OR next_attempt_at <= ?) ), ready AS ( SELECT candidates.* FROM candidates WHERE NOT ( candidates.resource_type = ? AND candidates.action = ? AND candidates.parent_external_key <> '' AND EXISTS ( SELECT 1 FROM worksmobile_outboxes parent_job WHERE parent_job.resource_type = ? AND parent_job.action = ? AND parent_job.status <> ? AND NULLIF(parent_job.payload #>> '{request,orgUnitExternalKey}', '') = candidates.parent_external_key ) ) ), org_depth AS ( SELECT id, org_external_key, parent_external_key, 0 AS depth FROM ready UNION ALL SELECT child.id, child.org_external_key, child.parent_external_key, parent.depth + 1 FROM ready child JOIN org_depth parent ON child.parent_external_key = parent.org_external_key WHERE child.resource_type = ? AND child.action = ? AND parent.depth < 64 ) SELECT ready.* FROM ready LEFT JOIN LATERAL ( SELECT max(depth) AS dependency_depth FROM org_depth WHERE org_depth.id = ready.id ) AS depth_rank ON true ORDER BY CASE WHEN ready.resource_type = ? AND ready.action = ? THEN 0 WHEN ready.resource_type = ? THEN 1 ELSE 2 END ASC, COALESCE(depth_rank.dependency_depth, 0) ASC, ready.created_at ASC LIMIT ? `, domain.WorksmobileOutboxStatusPending, time.Now(), domain.WorksmobileResourceOrgUnit, domain.WorksmobileActionUpsert, domain.WorksmobileResourceOrgUnit, domain.WorksmobileActionUpsert, domain.WorksmobileOutboxStatusProcessed, domain.WorksmobileResourceOrgUnit, domain.WorksmobileActionUpsert, domain.WorksmobileResourceOrgUnit, domain.WorksmobileActionUpsert, domain.WorksmobileResourceUser, limit, ).Scan(&rows).Error return rows, err } func (r *worksmobileOutboxRepository) FindByID(ctx context.Context, id string) (*domain.WorksmobileOutbox, error) { var row domain.WorksmobileOutbox if err := r.db.WithContext(ctx).First(&row, "id = ?", id).Error; err != nil { return nil, err } return &row, nil } func (r *worksmobileOutboxRepository) MarkRetry(ctx context.Context, id string) error { return r.db.WithContext(ctx).Model(&domain.WorksmobileOutbox{}).Where("id = ?", id).Updates(map[string]any{ "status": domain.WorksmobileOutboxStatusPending, "last_error": "", "next_attempt_at": nil, "updated_at": time.Now(), }).Error } func (r *worksmobileOutboxRepository) MarkProcessing(ctx context.Context, id string) (bool, error) { result := r.db.WithContext(ctx).Model(&domain.WorksmobileOutbox{}).Where("id = ? AND status = ?", id, domain.WorksmobileOutboxStatusPending).Updates(map[string]any{ "status": domain.WorksmobileOutboxStatusProcessing, "updated_at": time.Now(), }) return result.RowsAffected > 0, result.Error } func (r *worksmobileOutboxRepository) MarkProcessed(ctx context.Context, id string) error { now := time.Now() return r.db.WithContext(ctx).Model(&domain.WorksmobileOutbox{}).Where("id = ?", id).Updates(map[string]any{ "status": domain.WorksmobileOutboxStatusProcessed, "last_error": "", "processed_at": &now, "updated_at": now, }).Error } func (r *worksmobileOutboxRepository) MarkFailed(ctx context.Context, id string, message string, nextAttemptAt time.Time) error { return r.db.WithContext(ctx).Model(&domain.WorksmobileOutbox{}).Where("id = ?", id).Updates(map[string]any{ "status": domain.WorksmobileOutboxStatusFailed, "retry_count": gorm.Expr("retry_count + 1"), "last_error": message, "next_attempt_at": &nextAttemptAt, "updated_at": time.Now(), }).Error }