sentdm 0.32.0 → 0.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +9 -0
  3. data/README.md +1 -1
  4. data/lib/sentdm/client.rb +6 -0
  5. data/lib/sentdm/models/channel_event_payload.rb +139 -2
  6. data/lib/sentdm/models/contact_event.rb +15 -7
  7. data/lib/sentdm/models/contact_event_payload.rb +77 -16
  8. data/lib/sentdm/models/conversation_messages_list.rb +125 -4
  9. data/lib/sentdm/models/message_event_payload.rb +20 -1
  10. data/lib/sentdm/models/message_retrieve_activities_response.rb +20 -5
  11. data/lib/sentdm/models/message_retrieve_status_response.rb +133 -6
  12. data/lib/sentdm/models/message_send_params.rb +67 -3
  13. data/lib/sentdm/models/message_send_response.rb +13 -4
  14. data/lib/sentdm/models/template_body.rb +82 -1
  15. data/lib/sentdm/models/template_header.rb +91 -3
  16. data/lib/sentdm/models/template_variable.rb +15 -1
  17. data/lib/sentdm/models/webhook_list_events_response.rb +324 -9
  18. data/lib/sentdm/resources/messages.rb +33 -5
  19. data/lib/sentdm/version.rb +1 -1
  20. data/lib/sentdm.rb +1 -1
  21. data/rbi/sentdm/client.rbi +6 -0
  22. data/rbi/sentdm/models/channel_event_payload.rbi +253 -2
  23. data/rbi/sentdm/models/contact_event.rbi +28 -12
  24. data/rbi/sentdm/models/contact_event_payload.rbi +118 -28
  25. data/rbi/sentdm/models/conversation_messages_list.rbi +202 -7
  26. data/rbi/sentdm/models/message_event_payload.rbi +22 -0
  27. data/rbi/sentdm/models/message_retrieve_activities_response.rbi +23 -5
  28. data/rbi/sentdm/models/message_retrieve_status_response.rbi +214 -9
  29. data/rbi/sentdm/models/message_send_params.rbi +100 -2
  30. data/rbi/sentdm/models/message_send_response.rbi +15 -5
  31. data/rbi/sentdm/models/template_body.rbi +133 -0
  32. data/rbi/sentdm/models/template_header.rbi +143 -2
  33. data/rbi/sentdm/models/template_variable.rbi +10 -0
  34. data/rbi/sentdm/models/webhook_list_events_response.rbi +478 -12
  35. data/rbi/sentdm/resources/messages.rbi +58 -3
  36. data/sig/sentdm/models/channel_event_payload.rbs +57 -0
  37. data/sig/sentdm/models/contact_event_payload.rbs +24 -9
  38. data/sig/sentdm/models/conversation_messages_list.rbs +48 -3
  39. data/sig/sentdm/models/message_event_payload.rbs +10 -0
  40. data/sig/sentdm/models/message_retrieve_activities_response.rbs +5 -0
  41. data/sig/sentdm/models/message_retrieve_status_response.rbs +48 -3
  42. data/sig/sentdm/models/message_send_params.rbs +15 -0
  43. data/sig/sentdm/models/template_body.rbs +44 -0
  44. data/sig/sentdm/models/template_header.rbs +44 -0
  45. data/sig/sentdm/models/webhook_list_events_response.rbs +145 -0
  46. data/sig/sentdm/resources/messages.rbs +3 -0
  47. metadata +2 -2
@@ -17,6 +17,23 @@ module Sentdm
17
17
  sig { returns(T.nilable(T::Array[String])) }
18
18
  attr_accessor :channel
19
19
 
20
+ # Attachments for this send, as publicly fetchable https URLs. Used by the MMS
21
+ # channel and ignored by every other one.
22
+ #
23
+ # Supplying these replaces the media on the template's mms body rather than adding
24
+ # to it, so a template can hold a default creative while a caller still sends
25
+ # something recipient-specific.
26
+ #
27
+ # Their presence is also what makes a message eligible for MMS on an auto-detect
28
+ # send: a message with nothing attached is delivered as SMS, because an MMS with
29
+ # no media is a more expensive text message.
30
+ #
31
+ # The recipient's carrier fetches each URL after the send is accepted, so it must
32
+ # stay publicly reachable — a link that expires, or one behind auth, arrives as a
33
+ # failed message.
34
+ sig { returns(T.nilable(T::Array[String])) }
35
+ attr_accessor :media_urls
36
+
20
37
  # Sandbox flag - when true, the operation is simulated without side effects Useful
21
38
  # for testing integrations without actual execution
22
39
  sig { returns(T.nilable(T::Boolean)) }
@@ -25,6 +42,25 @@ module Sentdm
25
42
  sig { params(sandbox: T::Boolean).void }
26
43
  attr_writer :sandbox
27
44
 
45
+ # Optional future send time as an ISO-8601 timestamp with an explicit UTC offset,
46
+ # e.g. 2026-10-01T09:00:00+02:00 or 2026-10-01T07:00:00Z. A value without an
47
+ # offset is rejected (400) rather than read in the server's zone. The offset only
48
+ # fixes the instant: it is stored and echoed in UTC as scheduled_at. Omit to send
49
+ # now. Must be at least one minute ahead and at most 30 days ahead. Accepted
50
+ # messages report SCHEDULED and are released for delivery at this time. Quiet
51
+ # hours, balance and template approval are evaluated at release, not at
52
+ # acceptance: a message whose time falls inside a recipient's protected
53
+ # quiet-hours window is moved to the next allowed time and a second
54
+ # message.scheduled webhook reports the new scheduled_at.
55
+ sig { returns(T.nilable(Time)) }
56
+ attr_accessor :scheduled_at
57
+
58
+ # Subject line for this send, overriding the template's. MMS only; ignored on
59
+ # every other channel. Most handsets render it above the body, some ignore it
60
+ # entirely.
61
+ sig { returns(T.nilable(String)) }
62
+ attr_accessor :subject
63
+
28
64
  # SDK-style template reference: resolve by ID or by name, with optional
29
65
  # parameters.
30
66
  sig { returns(T.nilable(Sentdm::MessageSendParams::Template)) }
@@ -63,7 +99,10 @@ module Sentdm
63
99
  sig do
64
100
  params(
65
101
  channel: T.nilable(T::Array[String]),
102
+ media_urls: T.nilable(T::Array[String]),
66
103
  sandbox: T::Boolean,
104
+ scheduled_at: T.nilable(Time),
105
+ subject: T.nilable(String),
67
106
  template: T.nilable(Sentdm::MessageSendParams::Template::OrHash),
68
107
  text: T.nilable(String),
69
108
  to: T::Array[String],
@@ -77,9 +116,39 @@ module Sentdm
77
116
  # separate message per recipient. "sent" = auto-detect. Defaults to ["sent"]
78
117
  # (auto-detect) if omitted.
79
118
  channel: nil,
119
+ # Attachments for this send, as publicly fetchable https URLs. Used by the MMS
120
+ # channel and ignored by every other one.
121
+ #
122
+ # Supplying these replaces the media on the template's mms body rather than adding
123
+ # to it, so a template can hold a default creative while a caller still sends
124
+ # something recipient-specific.
125
+ #
126
+ # Their presence is also what makes a message eligible for MMS on an auto-detect
127
+ # send: a message with nothing attached is delivered as SMS, because an MMS with
128
+ # no media is a more expensive text message.
129
+ #
130
+ # The recipient's carrier fetches each URL after the send is accepted, so it must
131
+ # stay publicly reachable — a link that expires, or one behind auth, arrives as a
132
+ # failed message.
133
+ media_urls: nil,
80
134
  # Sandbox flag - when true, the operation is simulated without side effects Useful
81
135
  # for testing integrations without actual execution
82
136
  sandbox: nil,
137
+ # Optional future send time as an ISO-8601 timestamp with an explicit UTC offset,
138
+ # e.g. 2026-10-01T09:00:00+02:00 or 2026-10-01T07:00:00Z. A value without an
139
+ # offset is rejected (400) rather than read in the server's zone. The offset only
140
+ # fixes the instant: it is stored and echoed in UTC as scheduled_at. Omit to send
141
+ # now. Must be at least one minute ahead and at most 30 days ahead. Accepted
142
+ # messages report SCHEDULED and are released for delivery at this time. Quiet
143
+ # hours, balance and template approval are evaluated at release, not at
144
+ # acceptance: a message whose time falls inside a recipient's protected
145
+ # quiet-hours window is moved to the next allowed time and a second
146
+ # message.scheduled webhook reports the new scheduled_at.
147
+ scheduled_at: nil,
148
+ # Subject line for this send, overriding the template's. MMS only; ignored on
149
+ # every other channel. Most handsets render it above the body, some ignore it
150
+ # entirely.
151
+ subject: nil,
83
152
  # SDK-style template reference: resolve by ID or by name, with optional
84
153
  # parameters.
85
154
  template: nil,
@@ -97,7 +166,10 @@ module Sentdm
97
166
  override.returns(
98
167
  {
99
168
  channel: T.nilable(T::Array[String]),
169
+ media_urls: T.nilable(T::Array[String]),
100
170
  sandbox: T::Boolean,
171
+ scheduled_at: T.nilable(Time),
172
+ subject: T.nilable(String),
101
173
  template: T.nilable(Sentdm::MessageSendParams::Template),
102
174
  text: T.nilable(String),
103
175
  to: T::Array[String],
@@ -127,7 +199,20 @@ module Sentdm
127
199
  sig { returns(T.nilable(String)) }
128
200
  attr_accessor :name
129
201
 
130
- # Template variable parameters for personalization
202
+ # Template variable parameters for personalization, keyed by variable name.
203
+ #
204
+ # Every variable the template declares is required; GET /v3/templates/{id} lists
205
+ # them. Supplying a key the template does not declare is ignored.
206
+ #
207
+ # Media headers. A template whose header is an image (designed in WhatsApp Manager
208
+ # and imported into Sent) declares a reserved header_image key. Its value is a
209
+ # publicly reachable https URL that Meta fetches at send time — Sent does not host
210
+ # the asset, and the sample approved with the template is not reused. The key is
211
+ # derived from the header's media type, so header_video and header_document follow
212
+ # the same shape when those formats ship.
213
+ #
214
+ # "parameters": { "header_image": "https://cdn.example.com/banner.jpg", "name":
215
+ # "John Doe" }
131
216
  sig { returns(T.nilable(T::Hash[Symbol, String])) }
132
217
  attr_accessor :parameters
133
218
 
@@ -145,7 +230,20 @@ module Sentdm
145
230
  id: nil,
146
231
  # Template name (mutually exclusive with id)
147
232
  name: nil,
148
- # Template variable parameters for personalization
233
+ # Template variable parameters for personalization, keyed by variable name.
234
+ #
235
+ # Every variable the template declares is required; GET /v3/templates/{id} lists
236
+ # them. Supplying a key the template does not declare is ignored.
237
+ #
238
+ # Media headers. A template whose header is an image (designed in WhatsApp Manager
239
+ # and imported into Sent) declares a reserved header_image key. Its value is a
240
+ # publicly reachable https URL that Meta fetches at send time — Sent does not host
241
+ # the asset, and the sample approved with the template is not reused. The key is
242
+ # derived from the header's media type, so header_video and header_document follow
243
+ # the same shape when those formats ship.
244
+ #
245
+ # "parameters": { "header_image": "https://cdn.example.com/banner.jpg", "name":
246
+ # "John Doe" }
149
247
  parameters: nil
150
248
  )
151
249
  end
@@ -17,7 +17,9 @@ module Sentdm
17
17
  # its result; this is what a caller sees, and the mapping between them is a
18
18
  # decision the endpoint makes.
19
19
  #
20
- # The wire is unchanged by the move: same names, same values.
20
+ # The shape of an immediate send: it never has a scheduled_at key. A send that
21
+ # carried scheduled_at is a ScheduledSendMessageResponse, and the endpoint decides
22
+ # which of the two to answer with. From always returns this type.
21
23
  sig { returns(T.nilable(Sentdm::Models::MessageSendResponse::Data)) }
22
24
  attr_reader :data
23
25
 
@@ -68,7 +70,9 @@ module Sentdm
68
70
  # its result; this is what a caller sees, and the mapping between them is a
69
71
  # decision the endpoint makes.
70
72
  #
71
- # The wire is unchanged by the move: same names, same values.
73
+ # The shape of an immediate send: it never has a scheduled_at key. A send that
74
+ # carried scheduled_at is a ScheduledSendMessageResponse, and the endpoint decides
75
+ # which of the two to answer with. From always returns this type.
72
76
  data: nil,
73
77
  # Error information
74
78
  error: nil,
@@ -120,7 +124,9 @@ module Sentdm
120
124
  end
121
125
  attr_writer :recipients
122
126
 
123
- # Overall status — QUEUED once the batch is accepted for delivery.
127
+ # QUEUED: the batch is accepted. A request that carried scheduled_at is QUEUED
128
+ # here too; each message moves to SCHEDULED once it is held, as GET
129
+ # /v3/messages/{id} and the message.scheduled webhook report.
124
130
  sig { returns(T.nilable(String)) }
125
131
  attr_reader :status
126
132
 
@@ -148,7 +154,9 @@ module Sentdm
148
154
  # its result; this is what a caller sees, and the mapping between them is a
149
155
  # decision the endpoint makes.
150
156
  #
151
- # The wire is unchanged by the move: same names, same values.
157
+ # The shape of an immediate send: it never has a scheduled_at key. A send that
158
+ # carried scheduled_at is a ScheduledSendMessageResponse, and the endpoint decides
159
+ # which of the two to answer with. From always returns this type.
152
160
  sig do
153
161
  params(
154
162
  recipients:
@@ -162,7 +170,9 @@ module Sentdm
162
170
  end
163
171
  def self.new(
164
172
  recipients: nil,
165
- # Overall status — QUEUED once the batch is accepted for delivery.
173
+ # QUEUED: the batch is accepted. A request that carried scheduled_at is QUEUED
174
+ # here too; each message moves to SCHEDULED once it is held, as GET
175
+ # /v3/messages/{id} and the message.scheduled webhook report.
166
176
  status: nil,
167
177
  template_id: nil,
168
178
  template_name: nil
@@ -6,6 +6,19 @@ module Sentdm
6
6
  OrHash =
7
7
  T.type_alias { T.any(Sentdm::TemplateBody, Sentdm::Internal::AnyHash) }
8
8
 
9
+ # MMS-specific content — subject, text and attachments.
10
+ #
11
+ # Like Rcs, an override that cannot stand on its own: a template still needs a
12
+ # MultiChannel body or the Sms + Whatsapp pair to be deliverable at all. Unlike
13
+ # Rcs, it has no fallback at send time — MMS with no media is a more expensive
14
+ # SMS, so a template without this slot is deliberately not MMS-capable and never
15
+ # produces an MMS route candidate.
16
+ sig { returns(T.nilable(Sentdm::TemplateBody::Mms)) }
17
+ attr_reader :mms
18
+
19
+ sig { params(mms: T.nilable(Sentdm::TemplateBody::Mms::OrHash)).void }
20
+ attr_writer :mms
21
+
9
22
  # The shared body, used for every channel. One half of the choice described above.
10
23
  sig { returns(T.nilable(Sentdm::TemplateBodyContent)) }
11
24
  attr_reader :multi_channel
@@ -54,6 +67,7 @@ module Sentdm
54
67
  # the copy, but cannot stand alone.
55
68
  sig do
56
69
  params(
70
+ mms: T.nilable(Sentdm::TemplateBody::Mms::OrHash),
57
71
  multi_channel: T.nilable(Sentdm::TemplateBodyContent::OrHash),
58
72
  rcs: T.nilable(Sentdm::TemplateBodyContent::OrHash),
59
73
  sms: T.nilable(Sentdm::TemplateBodyContent::OrHash),
@@ -61,6 +75,14 @@ module Sentdm
61
75
  ).returns(T.attached_class)
62
76
  end
63
77
  def self.new(
78
+ # MMS-specific content — subject, text and attachments.
79
+ #
80
+ # Like Rcs, an override that cannot stand on its own: a template still needs a
81
+ # MultiChannel body or the Sms + Whatsapp pair to be deliverable at all. Unlike
82
+ # Rcs, it has no fallback at send time — MMS with no media is a more expensive
83
+ # SMS, so a template without this slot is deliberately not MMS-capable and never
84
+ # produces an MMS route candidate.
85
+ mms: nil,
64
86
  # The shared body, used for every channel. One half of the choice described above.
65
87
  multi_channel: nil,
66
88
  # RCS-specific copy that overrides the chosen strategy for RCS only. The one true
@@ -77,6 +99,7 @@ module Sentdm
77
99
  sig do
78
100
  override.returns(
79
101
  {
102
+ mms: T.nilable(Sentdm::TemplateBody::Mms),
80
103
  multi_channel: T.nilable(Sentdm::TemplateBodyContent),
81
104
  rcs: T.nilable(Sentdm::TemplateBodyContent),
82
105
  sms: T.nilable(Sentdm::TemplateBodyContent),
@@ -86,6 +109,116 @@ module Sentdm
86
109
  end
87
110
  def to_hash
88
111
  end
112
+
113
+ class Mms < Sentdm::Models::TemplateBodyContent
114
+ OrHash =
115
+ T.type_alias do
116
+ T.any(Sentdm::TemplateBody::Mms, Sentdm::Internal::AnyHash)
117
+ end
118
+
119
+ # Attachments carried by every send on this template, in order. A per-send
120
+ # media_urls on the request replaces this list rather than adding to it, so a
121
+ # template can hold a default creative and a caller can still send something
122
+ # recipient-specific.
123
+ sig { returns(T.nilable(T::Array[Sentdm::TemplateBody::Mms::Media])) }
124
+ attr_accessor :media
125
+
126
+ # MMS subject line. Optional — most handsets render it above the body, some ignore
127
+ # it entirely. Deliberately its own field rather than riding TemplateHeader: the
128
+ # header is authored once and shared across every channel, and carries Meta's
129
+ # 60-character cap plus its no-newline, no-emoji text rules, none of which
130
+ # describe an MMS subject.
131
+ sig { returns(T.nilable(String)) }
132
+ attr_accessor :subject
133
+
134
+ # MMS-specific content — subject, text and attachments.
135
+ #
136
+ # Like Rcs, an override that cannot stand on its own: a template still needs a
137
+ # MultiChannel body or the Sms + Whatsapp pair to be deliverable at all. Unlike
138
+ # Rcs, it has no fallback at send time — MMS with no media is a more expensive
139
+ # SMS, so a template without this slot is deliberately not MMS-capable and never
140
+ # produces an MMS route candidate.
141
+ sig do
142
+ params(
143
+ media:
144
+ T.nilable(T::Array[Sentdm::TemplateBody::Mms::Media::OrHash]),
145
+ subject: T.nilable(String)
146
+ ).returns(T.attached_class)
147
+ end
148
+ def self.new(
149
+ # Attachments carried by every send on this template, in order. A per-send
150
+ # media_urls on the request replaces this list rather than adding to it, so a
151
+ # template can hold a default creative and a caller can still send something
152
+ # recipient-specific.
153
+ media: nil,
154
+ # MMS subject line. Optional — most handsets render it above the body, some ignore
155
+ # it entirely. Deliberately its own field rather than riding TemplateHeader: the
156
+ # header is authored once and shared across every channel, and carries Meta's
157
+ # 60-character cap plus its no-newline, no-emoji text rules, none of which
158
+ # describe an MMS subject.
159
+ subject: nil
160
+ )
161
+ end
162
+
163
+ sig do
164
+ override.returns(
165
+ {
166
+ media: T.nilable(T::Array[Sentdm::TemplateBody::Mms::Media]),
167
+ subject: T.nilable(String)
168
+ }
169
+ )
170
+ end
171
+ def to_hash
172
+ end
173
+
174
+ class Media < Sentdm::Internal::Type::BaseModel
175
+ OrHash =
176
+ T.type_alias do
177
+ T.any(Sentdm::TemplateBody::Mms::Media, Sentdm::Internal::AnyHash)
178
+ end
179
+
180
+ # One of MmsMediaTypes. Advisory: the carrier reads the Content-Type off the
181
+ # fetched object, not this field. It exists so an authoring UI can render the
182
+ # right preview and so a reviewer can see what was intended.
183
+ sig { returns(T.nilable(String)) }
184
+ attr_accessor :media_type
185
+
186
+ # Publicly fetchable https URL. The carrier's MMSC fetches this at send time, so
187
+ # it has to stay reachable and unauthenticated for the life of the send —
188
+ # including retries and a DLQ replay — which is why a presigned URL is not a valid
189
+ # value here.
190
+ sig { returns(T.nilable(String)) }
191
+ attr_reader :url
192
+
193
+ sig { params(url: String).void }
194
+ attr_writer :url
195
+
196
+ # One attachment on an MMS template body.
197
+ sig do
198
+ params(media_type: T.nilable(String), url: String).returns(
199
+ T.attached_class
200
+ )
201
+ end
202
+ def self.new(
203
+ # One of MmsMediaTypes. Advisory: the carrier reads the Content-Type off the
204
+ # fetched object, not this field. It exists so an authoring UI can render the
205
+ # right preview and so a reviewer can see what was intended.
206
+ media_type: nil,
207
+ # Publicly fetchable https URL. The carrier's MMSC fetches this at send time, so
208
+ # it has to stay reachable and unauthenticated for the life of the send —
209
+ # including retries and a DLQ replay — which is why a presigned URL is not a valid
210
+ # value here.
211
+ url: nil
212
+ )
213
+ end
214
+
215
+ sig do
216
+ override.returns({ media_type: T.nilable(String), url: String })
217
+ end
218
+ def to_hash
219
+ end
220
+ end
221
+ end
89
222
  end
90
223
  end
91
224
  end
@@ -13,7 +13,59 @@ module Sentdm
13
13
  sig { returns(String) }
14
14
  attr_accessor :template
15
15
 
16
- # The type of header (e.g., "text", "image", "video", "document")
16
+ # Request-only. The s.dm URL of the asset Meta's reviewers see —
17
+ # https://s.dm/s/{ID}, eight uppercase characters, uploaded to s.dm out of band.
18
+ # NormalizeRichHeader folds it into the synthesized media variable's Props.Sample
19
+ # and clears it, so it never persists and a stored definition is indistinguishable
20
+ # from an imported one.
21
+ #
22
+ # Stricter than the send path on purpose:
23
+ # TemplateUtils.ValidateMediaVariableValues accepts any absolute https URL for the
24
+ # per-send asset, because that one is the customer's and may live behind a signed
25
+ # CDN link. This one is the review sample, has to outlive every resubmission, and
26
+ # so must be ours. Do not "fix" one to match the other.
27
+ sig { returns(T.nilable(String)) }
28
+ attr_accessor :example_url
29
+
30
+ # The map pin a location header drops. Meta wants none of this at creation — the
31
+ # component is just {"type":"header","format":"location"} — so these values exist
32
+ # for Sent: a preview, and the default a StaticResource header falls back to at
33
+ # send.
34
+ sig { returns(T.nilable(Sentdm::TemplateHeader::Location)) }
35
+ attr_reader :location
36
+
37
+ sig do
38
+ params(
39
+ location: T.nilable(Sentdm::TemplateHeader::Location::OrHash)
40
+ ).void
41
+ end
42
+ attr_writer :location
43
+
44
+ # Whether the asset registered at creation is reused when a caller omits the
45
+ # header's variable at send time. Default false — the caller must supply it per
46
+ # message, which is the behaviour every existing template has. Written only when
47
+ # true, so a default-valued header serializes byte-identically to one imported
48
+ # from Meta.
49
+ #
50
+ # Stored and validated but not yet honoured at send: that lands with the Resumable
51
+ # Upload work, alongside the code that lets such a template be approved in the
52
+ # first place.
53
+ sig { returns(T.nilable(T::Boolean)) }
54
+ attr_reader :static_resource
55
+
56
+ sig { params(static_resource: T::Boolean).void }
57
+ attr_writer :static_resource
58
+
59
+ # The kind of header. One of:
60
+ #
61
+ # text — up to 60 characters, at most one variable. image — png, jpg or jpeg.
62
+ # Needs ExampleUrl. video — mp4. Needs ExampleUrl. gif — mp4, max 3.5MB. WhatsApp
63
+ # renders larger files as an ordinary video. Needs ExampleUrl. document — pdf or
64
+ # docx; only the first page is shown as a thumbnail, so pdf is the practical
65
+ # choice. Needs ExampleUrl. location — a map pin, supplied through Location.
66
+ #
67
+ # Kept lowercase because MetaToTemplateConverter writes Meta's format through
68
+ # ToLowerInvariant() into this field on import, and the two are compared directly.
17
69
  sig { returns(T.nilable(String)) }
18
70
  attr_accessor :type
19
71
 
@@ -25,6 +77,9 @@ module Sentdm
25
77
  sig do
26
78
  params(
27
79
  template: String,
80
+ example_url: T.nilable(String),
81
+ location: T.nilable(Sentdm::TemplateHeader::Location::OrHash),
82
+ static_resource: T::Boolean,
28
83
  type: T.nilable(String),
29
84
  variables: T.nilable(T::Array[Sentdm::TemplateVariable::OrHash])
30
85
  ).returns(T.attached_class)
@@ -33,7 +88,43 @@ module Sentdm
33
88
  # The header template text with optional variable placeholders (e.g., "Welcome to
34
89
  # {{0:variable}}")
35
90
  template:,
36
- # The type of header (e.g., "text", "image", "video", "document")
91
+ # Request-only. The s.dm URL of the asset Meta's reviewers see —
92
+ # https://s.dm/s/{ID}, eight uppercase characters, uploaded to s.dm out of band.
93
+ # NormalizeRichHeader folds it into the synthesized media variable's Props.Sample
94
+ # and clears it, so it never persists and a stored definition is indistinguishable
95
+ # from an imported one.
96
+ #
97
+ # Stricter than the send path on purpose:
98
+ # TemplateUtils.ValidateMediaVariableValues accepts any absolute https URL for the
99
+ # per-send asset, because that one is the customer's and may live behind a signed
100
+ # CDN link. This one is the review sample, has to outlive every resubmission, and
101
+ # so must be ours. Do not "fix" one to match the other.
102
+ example_url: nil,
103
+ # The map pin a location header drops. Meta wants none of this at creation — the
104
+ # component is just {"type":"header","format":"location"} — so these values exist
105
+ # for Sent: a preview, and the default a StaticResource header falls back to at
106
+ # send.
107
+ location: nil,
108
+ # Whether the asset registered at creation is reused when a caller omits the
109
+ # header's variable at send time. Default false — the caller must supply it per
110
+ # message, which is the behaviour every existing template has. Written only when
111
+ # true, so a default-valued header serializes byte-identically to one imported
112
+ # from Meta.
113
+ #
114
+ # Stored and validated but not yet honoured at send: that lands with the Resumable
115
+ # Upload work, alongside the code that lets such a template be approved in the
116
+ # first place.
117
+ static_resource: nil,
118
+ # The kind of header. One of:
119
+ #
120
+ # text — up to 60 characters, at most one variable. image — png, jpg or jpeg.
121
+ # Needs ExampleUrl. video — mp4. Needs ExampleUrl. gif — mp4, max 3.5MB. WhatsApp
122
+ # renders larger files as an ordinary video. Needs ExampleUrl. document — pdf or
123
+ # docx; only the first page is shown as a thumbnail, so pdf is the practical
124
+ # choice. Needs ExampleUrl. location — a map pin, supplied through Location.
125
+ #
126
+ # Kept lowercase because MetaToTemplateConverter writes Meta's format through
127
+ # ToLowerInvariant() into this field on import, and the two are compared directly.
37
128
  type: nil,
38
129
  # List of variables used in the header template
39
130
  variables: nil
@@ -44,6 +135,9 @@ module Sentdm
44
135
  override.returns(
45
136
  {
46
137
  template: String,
138
+ example_url: T.nilable(String),
139
+ location: T.nilable(Sentdm::TemplateHeader::Location),
140
+ static_resource: T::Boolean,
47
141
  type: T.nilable(String),
48
142
  variables: T.nilable(T::Array[Sentdm::TemplateVariable])
49
143
  }
@@ -51,6 +145,53 @@ module Sentdm
51
145
  end
52
146
  def to_hash
53
147
  end
148
+
149
+ class Location < Sentdm::Internal::Type::BaseModel
150
+ OrHash =
151
+ T.type_alias do
152
+ T.any(Sentdm::TemplateHeader::Location, Sentdm::Internal::AnyHash)
153
+ end
154
+
155
+ sig { returns(String) }
156
+ attr_accessor :address
157
+
158
+ sig { returns(String) }
159
+ attr_accessor :latitude
160
+
161
+ sig { returns(String) }
162
+ attr_accessor :longitude
163
+
164
+ sig { returns(String) }
165
+ attr_accessor :name
166
+
167
+ # The map pin a location header drops. Meta wants none of this at creation — the
168
+ # component is just {"type":"header","format":"location"} — so these values exist
169
+ # for Sent: a preview, and the default a StaticResource header falls back to at
170
+ # send.
171
+ sig do
172
+ params(
173
+ address: String,
174
+ latitude: String,
175
+ longitude: String,
176
+ name: String
177
+ ).returns(T.attached_class)
178
+ end
179
+ def self.new(address:, latitude:, longitude:, name:)
180
+ end
181
+
182
+ sig do
183
+ override.returns(
184
+ {
185
+ address: String,
186
+ latitude: String,
187
+ longitude: String,
188
+ name: String
189
+ }
190
+ )
191
+ end
192
+ def to_hash
193
+ end
194
+ end
54
195
  end
55
196
  end
56
197
  end
@@ -86,6 +86,11 @@ module Sentdm
86
86
  sig { returns(String) }
87
87
  attr_accessor :media_type
88
88
 
89
+ # Example value substituted into the template when previewing it and when
90
+ # submitting it to Meta for review. Free text by nature, so the converter accepts
91
+ # a JSON number or boolean here and normalizes it — see
92
+ # JsonScalarToStringConverter for why — and guarantees it is always serialized
93
+ # back out as a JSON string.
89
94
  sig { returns(String) }
90
95
  attr_accessor :sample
91
96
 
@@ -117,6 +122,11 @@ module Sentdm
117
122
  end
118
123
  def self.new(
119
124
  media_type:,
125
+ # Example value substituted into the template when previewing it and when
126
+ # submitting it to Meta for review. Free text by nature, so the converter accepts
127
+ # a JSON number or boolean here and normalizes it — see
128
+ # JsonScalarToStringConverter for why — and guarantees it is always serialized
129
+ # back out as a JSON string.
120
130
  sample:,
121
131
  url:,
122
132
  variable_type:,