package media import ( "context" "errors" "strings" "time" "data-terminal-cloud/internal/audit" "data-terminal-cloud/internal/database" "data-terminal-cloud/internal/deviceauth" "github.com/jackc/pgx/v5" ) // AcknowledgeLocalDelete records only deletion of the device-side encrypted copy. // The verified cloud object and its media_segment lifecycle remain unchanged. func (s *Service) AcknowledgeLocalDelete(ctx context.Context, identity deviceauth.Identity, segmentPublicID, status, errorCode, requestID string) (Segment, error) { status = strings.ToUpper(strings.TrimSpace(status)) errorCode = strings.ToUpper(strings.TrimSpace(errorCode)) if status != "DELETED" && status != "FAILED" { return Segment{}, ErrInvalidDeclaration } if status == "DELETED" && errorCode != "" { return Segment{}, ErrInvalidDeclaration } if status == "FAILED" && (errorCode == "" || len(errorCode) > 80) { return Segment{}, ErrInvalidDeclaration } var result Segment err := database.Transact(ctx, s.pool, func(tx pgx.Tx) error { var cloudStatus, storedStatus string var deleteAfter *time.Time err := tx.QueryRow(ctx, ` SELECT status,COALESCE(local_delete_status,''),local_delete_after FROM media_segment WHERE public_id=$1::uuid AND tenant_id=$2::uuid AND device_id=$3::uuid FOR UPDATE`, segmentPublicID, identity.TenantID, identity.ID, ).Scan(&cloudStatus, &storedStatus, &deleteAfter) if errors.Is(err, pgx.ErrNoRows) { return ErrNotFound } if err != nil { return err } if cloudStatus != "VERIFIED" || deleteAfter == nil { return ErrConflict } if storedStatus == "DELETED" { return scanDeviceSegment(ctx, tx, identity, segmentPublicID, &result) } if status == "DELETED" && time.Now().UTC().Before(*deleteAfter) { return ErrLocalDeleteTooEarly } if _, err := tx.Exec(ctx, ` UPDATE media_segment SET local_delete_status=$4,local_delete_reported_at=now(),local_delete_error_code=NULLIF($5,''),updated_at=now() WHERE public_id=$1::uuid AND tenant_id=$2::uuid AND device_id=$3::uuid`, segmentPublicID, identity.TenantID, identity.ID, status, errorCode, ); err != nil { return err } auditResult := "success" if status == "FAILED" { auditResult = "failure" } if err := s.audit.Record(ctx, tx, audit.Event{ TenantID: &identity.TenantID, ActorType: "device", ActorPublicID: &identity.PublicID, Action: "media.local_delete.ack", ResourceType: "media_segment", ResourceID: &segmentPublicID, RequestID: requestID, Result: auditResult, Detail: map[string]any{"status": status, "error_code": errorCode}, }); err != nil { return err } return scanDeviceSegment(ctx, tx, identity, segmentPublicID, &result) }) return result, err } func scanDeviceSegment(ctx context.Context, tx pgx.Tx, identity deviceauth.Identity, segmentPublicID string, result *Segment) error { return tx.QueryRow(ctx, ` SELECT public_id::text,track,sequence_no,codec,content_type,duration_ms,expected_size,expected_digest,status, COALESCE(last_error_code,''),verified_at,local_delete_after,COALESCE(local_delete_status,''), local_delete_reported_at,COALESCE(local_delete_error_code,'') FROM media_segment WHERE public_id=$1::uuid AND tenant_id=$2::uuid AND device_id=$3::uuid`, segmentPublicID, identity.TenantID, identity.ID, ).Scan(&result.ID, &result.Track, &result.SequenceNo, &result.Codec, &result.ContentType, &result.DurationMS, &result.ExpectedSize, &result.ExpectedDigest, &result.Status, &result.LastErrorCode, &result.VerifiedAt, &result.LocalDeleteAfter, &result.LocalDeleteStatus, &result.LocalDeleteReportedAt, &result.LocalDeleteErrorCode) }