mirror of
https://github.com/warmbly/warmbly.git
synced 2026-10-05 16:02:10 +00:00
Merge pull request #818 from warmbly/claude/api-contract-2026-10-04
feat: describe the stored send-plan snapshot in the OpenAPI document (stale flag on CampaignSendPlan)
This commit is contained in:
@@ -6215,7 +6215,7 @@
|
||||
"get": {
|
||||
"operationId": "campaigns_send_plan",
|
||||
"summary": "Today's sending plan",
|
||||
"description": "What the campaign sends today and every limit that decided it, worked out through the scheduler's own gates. Nothing is stored or written. Scope READ_CAMPAIGNS, org permission view_campaigns.",
|
||||
"description": "What the campaign sends today and every limit that decided it, worked out through the scheduler's own gates. An active campaign is served from a snapshot computed in the background (`computed_at` says when, `stale` says a refresh is pending); a campaign with no snapshot yet is computed on the request. Scope READ_CAMPAIGNS, org permission view_campaigns.",
|
||||
"tags": [
|
||||
"campaigns"
|
||||
],
|
||||
@@ -30540,13 +30540,14 @@
|
||||
},
|
||||
"CampaignSendPlan": {
|
||||
"type": "object",
|
||||
"description": "Today's sending plan for a campaign, derived through the scheduler's gates on every read.",
|
||||
"description": "Today's sending plan for a campaign, derived through the scheduler's gates and served from a background snapshot for an active campaign.",
|
||||
"required": [
|
||||
"campaign_id",
|
||||
"status",
|
||||
"day",
|
||||
"timezone",
|
||||
"computed_at",
|
||||
"stale",
|
||||
"configured_ceiling",
|
||||
"projected_today",
|
||||
"sent_today",
|
||||
@@ -30574,7 +30575,12 @@
|
||||
},
|
||||
"computed_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
"format": "date-time",
|
||||
"description": "When this plan was computed. For a snapshot, the moment it was last walked."
|
||||
},
|
||||
"stale": {
|
||||
"type": "boolean",
|
||||
"description": "True when the snapshot has been outrun by an edit, a start or stop, or a new budget day, or has grown older than the snapshot's maximum age even though nothing changed, and a refreshed plan is already being computed. Always false for a plan computed on the request."
|
||||
},
|
||||
"configured_ceiling": {
|
||||
"type": "integer",
|
||||
|
||||
@@ -1338,7 +1338,7 @@ func (r *campaignRepository) Update(ctx context.Context, orgID, campaignID strin
|
||||
}()
|
||||
|
||||
var query string
|
||||
if argPos > 3 {
|
||||
if argPos >= 3 {
|
||||
query = fmt.Sprintf(`
|
||||
UPDATE campaigns
|
||||
SET %s
|
||||
@@ -2183,6 +2183,12 @@ func (r *campaignRepository) ReplaceCampaignSenders(ctx context.Context, campaig
|
||||
return nil, xerr
|
||||
}
|
||||
|
||||
// The send-plan snapshot is keyed on updated_at, so a sender edit must move it.
|
||||
if _, err := tx.Exec(ctx, `UPDATE campaigns SET updated_at = now() WHERE id = $1`, campaignID); err != nil {
|
||||
db.CaptureError(err, "campaign updated_at", []any{campaignID}, "exec")
|
||||
return nil, errx.InternalError()
|
||||
}
|
||||
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
db.CaptureError(err, "", nil, "commit")
|
||||
return nil, errx.InternalError()
|
||||
|
||||
@@ -655,10 +655,18 @@ func setForCampaignTx(ctx context.Context, tx pgx.Tx, orgID, campaignID uuid.UUI
|
||||
return "", change, xerr
|
||||
}
|
||||
if len(segmentIDs) > 0 {
|
||||
if _, err := tx.Exec(ctx, `INSERT INTO campaign_segments (campaign_id, segment_id) SELECT $1, unnest($2::uuid[]) ON CONFLICT DO NOTHING`, campaignID, segmentIDs); err != nil {
|
||||
tag, err := tx.Exec(ctx, `INSERT INTO campaign_segments (campaign_id, segment_id) SELECT $1, unnest($2::uuid[]) ON CONFLICT DO NOTHING`, campaignID, segmentIDs)
|
||||
if err != nil {
|
||||
db.CaptureError(err, "campaign segments insert", nil, "exec")
|
||||
return "", change, errx.InternalError()
|
||||
}
|
||||
// New links change the send plan the snapshot is keyed on.
|
||||
if tag.RowsAffected() > 0 {
|
||||
if _, err := tx.Exec(ctx, `UPDATE campaigns SET updated_at = NOW() WHERE id = $1`, campaignID); err != nil {
|
||||
db.CaptureError(err, "campaign updated_at", nil, "exec")
|
||||
return "", change, errx.InternalError()
|
||||
}
|
||||
}
|
||||
// A live audience is the reason to keep running: linking turns the
|
||||
// setting on, and the owner can turn it off again in preferences.
|
||||
if _, err := tx.Exec(ctx, `UPDATE campaigns SET continuous = true, updated_at = NOW() WHERE id = $1 AND NOT continuous`, campaignID); err != nil {
|
||||
@@ -666,6 +674,13 @@ func setForCampaignTx(ctx context.Context, tx pgx.Tx, orgID, campaignID uuid.UUI
|
||||
return "", change, errx.InternalError()
|
||||
}
|
||||
}
|
||||
// Detaching withdraws leads, which changes the send plan the snapshot is keyed on.
|
||||
if len(detached) > 0 {
|
||||
if _, err := tx.Exec(ctx, `UPDATE campaigns SET updated_at = NOW() WHERE id = $1`, campaignID); err != nil {
|
||||
db.CaptureError(err, "campaign updated_at", nil, "exec")
|
||||
return "", change, errx.InternalError()
|
||||
}
|
||||
}
|
||||
return status, change, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user