|
1 | 1 | # Scheduling Send Refactor Design |
2 | 2 |
|
| 3 | +## Related Documentation |
| 4 | + |
| 5 | +- [Scheduling SMS Messages](https://docs.httpsms.com/features/scheduling-sms-messages) — existing `SendAt`/`SendTime` scheduling feature |
| 6 | +- [Control SMS Send Rate](https://docs.httpsms.com/features/control-sms-send-rate) — existing `MessagesPerMinute` rate-limiting feature |
| 7 | + |
3 | 8 | ## Problem Statement |
4 | 9 |
|
5 | 10 | The current SMS scheduling logic has two issues: |
6 | 11 |
|
7 | | -1. **No way to send at an exact time without scheduling interference.** When a user specifies a `SendTime`/`SendAt`, the system still applies rate-limiting and schedule window logic, which may shift the actual send time. |
| 12 | +1. **No way to send at an exact time without scheduling interference.** When a user specifies a `SendTime`/`SendAt` (see [Scheduling SMS Messages](https://docs.httpsms.com/features/scheduling-sms-messages)), the system still applies rate-limiting and schedule window logic, which may shift the actual send time. |
8 | 13 |
|
9 | 14 | 2. **Bulk message contention.** When bulk messages (API or CSV) are sent, all events arrive at the Cloud Tasks queue near-simultaneously, causing DB serialization conflicts in `PhoneNotificationRepository.Schedule()` (which uses `SELECT ... ORDER BY scheduled_at DESC` in a transaction). The current workaround is a hardcoded 1-second spacing hack. |
10 | 15 |
|
11 | 16 | ## Proposed Solution |
12 | 17 |
|
13 | 18 | ### Core Principle |
14 | 19 |
|
15 | | -- **Explicit `SendTime`** = send at exactly that time, bypass all scheduling logic. |
16 | | -- **No `SendTime`** = apply full scheduling logic (rate-limit + schedule windows), with rate-based Cloud Task dispatch delay to prevent DB contention. |
| 20 | +- **Explicit `SendTime`** = send at exactly that time, bypass all scheduling logic. See [Scheduling SMS Messages](https://docs.httpsms.com/features/scheduling-sms-messages) for how `SendAt` works. |
| 21 | +- **No `SendTime`** = apply full scheduling logic ([rate-limit](https://docs.httpsms.com/features/control-sms-send-rate) + schedule windows), with rate-based Cloud Task dispatch delay to prevent DB contention. |
17 | 22 |
|
18 | 23 | ### Design |
19 | 24 |
|
@@ -111,7 +116,73 @@ User sends request |
111 | 116 | ### What Does NOT Change |
112 | 117 |
|
113 | 118 | - The `MessageSendSchedule` entity and its `ResolveScheduledAt()` logic |
114 | | -- The `SendScheduleService` CRUD operations |
| 119 | +- The `MessageSendScheduleService` CRUD operations |
115 | 120 | - The phone notification entity schema (no new DB columns) |
116 | 121 | - The Android app behavior |
117 | 122 | - The web frontend (models auto-generated from Swagger) |
| 123 | + |
| 124 | +--- |
| 125 | + |
| 126 | +## MessageSendSchedule (Send Windows) — New Feature |
| 127 | + |
| 128 | +This is the only scheduling mechanism that does **not** have a dedicated documentation page yet. Unlike [Scheduling SMS Messages](https://docs.httpsms.com/features/scheduling-sms-messages) (one-time `SendAt`) and [Control SMS Send Rate](https://docs.httpsms.com/features/control-sms-send-rate) (`MessagesPerMinute` throttling), MessageSendSchedule defines **recurring availability windows** that control when a phone is allowed to send outgoing SMS messages. |
| 129 | + |
| 130 | +### Concept |
| 131 | + |
| 132 | +A `MessageSendSchedule` is a named set of time windows (per day of week) that define when the phone can send. Messages arriving outside those windows are delayed until the next available window opens. |
| 133 | + |
| 134 | +### Entity |
| 135 | + |
| 136 | +```go |
| 137 | +type MessageSendSchedule struct { |
| 138 | + ID uuid.UUID |
| 139 | + UserID UserID |
| 140 | + Name string // e.g. "Business Hours" |
| 141 | + Timezone string // IANA timezone e.g. "Europe/Tallinn" |
| 142 | + IsActive bool |
| 143 | + Windows []MessageSendScheduleWindow // per-day availability slots |
| 144 | + CreatedAt time.Time |
| 145 | + UpdatedAt time.Time |
| 146 | +} |
| 147 | + |
| 148 | +type MessageSendScheduleWindow struct { |
| 149 | + DayOfWeek int // 0=Sunday, 6=Saturday |
| 150 | + StartMinute int // minutes from midnight (e.g. 540 = 9:00) |
| 151 | + EndMinute int // minutes from midnight (e.g. 1020 = 17:00) |
| 152 | +} |
| 153 | +``` |
| 154 | + |
| 155 | +### How It Works |
| 156 | + |
| 157 | +1. A user creates a schedule via `POST /v1/send-schedules` with a name, timezone, and one or more windows. |
| 158 | +2. The schedule is linked to a phone via a `ScheduleID` field on the phone entity. |
| 159 | +3. When a message is queued (without an explicit `SendAt`), the `PhoneNotificationRepository.Schedule()` method calls `MessageSendSchedule.ResolveScheduledAt(now)` to find the next allowed send time. |
| 160 | +4. If the current time falls within a window, the message sends immediately. If not, it's delayed to the start of the next available window. |
| 161 | + |
| 162 | +### API Endpoints |
| 163 | + |
| 164 | +| Method | Endpoint | Description | |
| 165 | +| ------ | --------------------------------- | --------------------------- | |
| 166 | +| GET | `/v1/send-schedules` | List all user schedules | |
| 167 | +| POST | `/v1/send-schedules` | Create a new schedule | |
| 168 | +| PUT | `/v1/send-schedules/{scheduleID}` | Update an existing schedule | |
| 169 | +| DELETE | `/v1/send-schedules/{scheduleID}` | Delete a schedule | |
| 170 | + |
| 171 | +### Validation Rules |
| 172 | + |
| 173 | +- `name`: required, 2–100 characters |
| 174 | +- `timezone`: required, valid IANA timezone |
| 175 | +- `windows[].day_of_week`: 0–6 |
| 176 | +- `windows[].start_minute`: 0–1439 |
| 177 | +- `windows[].end_minute`: 1–1440, must be greater than `start_minute` |
| 178 | +- Max 6 windows per day |
| 179 | +- No overlapping windows on the same day |
| 180 | + |
| 181 | +### Entitlement |
| 182 | + |
| 183 | +Free users are limited to 1 schedule. Paid users get unlimited schedules. Enforced via `EntitlementService.Check()` in the handler before creation. |
| 184 | + |
| 185 | +### Interaction with Other Scheduling Features |
| 186 | + |
| 187 | +- **[Scheduling SMS Messages](https://docs.httpsms.com/features/scheduling-sms-messages)** (`SendAt`): When provided, bypasses send windows entirely (exact send time). |
| 188 | +- **[Control SMS Send Rate](https://docs.httpsms.com/features/control-sms-send-rate)** (`MessagesPerMinute`): Applied independently — rate-limiting still applies within allowed windows. Both constraints compose: the message must be within a window AND respect the rate limit. |
0 commit comments