source;
private ConversationMessageTag tag;
+ private ConversationMessageMetadata metadata;
/**
* See: {@link ConversationPlatformConstants}
*/
@@ -115,6 +116,14 @@ public void setTag(ConversationMessageTag tag) {
this.tag = tag;
}
+ public ConversationMessageMetadata getMetadata() {
+ return metadata;
+ }
+
+ public void setMetadata(ConversationMessageMetadata metadata) {
+ this.metadata = metadata;
+ }
+
public String getPlatform() {
return platform;
}
@@ -146,6 +155,7 @@ public String toString() {
", updatedDatetime=" + updatedDatetime +
", source=" + source +
", tag=" + tag +
+ ", metadata=" + metadata +
", platform='" + platform + '\'' +
'}';
}
diff --git a/api/src/main/java/com/messagebird/objects/conversations/ConversationMessageMetadata.java b/api/src/main/java/com/messagebird/objects/conversations/ConversationMessageMetadata.java
new file mode 100644
index 00000000..c41e814b
--- /dev/null
+++ b/api/src/main/java/com/messagebird/objects/conversations/ConversationMessageMetadata.java
@@ -0,0 +1,42 @@
+package com.messagebird.objects.conversations;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+import java.util.Date;
+
+/**
+ * Inner metadata attached to a conversation message. Present on both incoming
+ * messages and status webhook payloads. {@code sender.userId} always contains
+ * the BSUID when Meta provides one. When both identifiers exist, the phone
+ * number appears in the parent {@code from} field, not in this object.
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class ConversationMessageMetadata {
+
+ private ConversationSenderMetadata sender;
+ private Date receivedAt;
+
+ public ConversationSenderMetadata getSender() {
+ return sender;
+ }
+
+ public void setSender(ConversationSenderMetadata sender) {
+ this.sender = sender;
+ }
+
+ public Date getReceivedAt() {
+ return receivedAt;
+ }
+
+ public void setReceivedAt(Date receivedAt) {
+ this.receivedAt = receivedAt;
+ }
+
+ @Override
+ public String toString() {
+ return "ConversationMessageMetadata{" +
+ "sender=" + sender +
+ ", receivedAt=" + receivedAt +
+ '}';
+ }
+}
diff --git a/api/src/main/java/com/messagebird/objects/conversations/ConversationRecipientMetadata.java b/api/src/main/java/com/messagebird/objects/conversations/ConversationRecipientMetadata.java
new file mode 100644
index 00000000..7537da85
--- /dev/null
+++ b/api/src/main/java/com/messagebird/objects/conversations/ConversationRecipientMetadata.java
@@ -0,0 +1,50 @@
+package com.messagebird.objects.conversations;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+/**
+ * Identifies the recipient of an outbound WhatsApp message, as reported back on
+ * status webhook payloads under {@code status.metadata.recipient}. Mirrors
+ * {@link ConversationSenderMetadata} on the inbound side.
+ *
+ * {@code userId} is the recipient's BSUID (e.g. "US.13491208655302741918");
+ * {@code parentUserId} is the parent business-scoped user ID of the enterprise
+ * that owns the business portfolio it was scoped against (e.g.
+ * "US.ENT.11815799212886844830").
+ *
+ *
Either field may be {@code null}: Meta only supplies them for accounts
+ * enrolled in the BSUID rollout, and the enclosing {@code recipient} object is
+ * omitted entirely when neither is present. This is the only place a status
+ * payload carries the recipient's own identity — {@code messageMetadata.to} is
+ * an echo of the address the message was addressed to.
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class ConversationRecipientMetadata {
+
+ private String userId;
+ private String parentUserId;
+
+ public String getUserId() {
+ return userId;
+ }
+
+ public void setUserId(String userId) {
+ this.userId = userId;
+ }
+
+ public String getParentUserId() {
+ return parentUserId;
+ }
+
+ public void setParentUserId(String parentUserId) {
+ this.parentUserId = parentUserId;
+ }
+
+ @Override
+ public String toString() {
+ return "ConversationRecipientMetadata{" +
+ "userId='" + userId + '\'' +
+ ", parentUserId='" + parentUserId + '\'' +
+ '}';
+ }
+}
diff --git a/api/src/main/java/com/messagebird/objects/conversations/ConversationSenderMetadata.java b/api/src/main/java/com/messagebird/objects/conversations/ConversationSenderMetadata.java
new file mode 100644
index 00000000..b7fdcbb0
--- /dev/null
+++ b/api/src/main/java/com/messagebird/objects/conversations/ConversationSenderMetadata.java
@@ -0,0 +1,66 @@
+package com.messagebird.objects.conversations;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+/**
+ * Metadata about the sender of a WhatsApp message. {@code userId} always
+ * contains the BSUID (e.g. "US.13491208655302741918") when Meta supplies one.
+ * When both a phone number and a BSUID are available, the phone number appears
+ * in the parent message's {@code from} field — not here.
+ *
+ *
{@code parentUserId} carries the sender's parent business-scoped user ID
+ * (e.g. "US.ENT.11815799212886844830"), which identifies the enterprise that
+ * owns the business portfolio the {@code userId} was scoped against. It is only
+ * present for accounts enrolled in Meta's parent-BSUID rollout; for everyone
+ * else it stays {@code null}.
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class ConversationSenderMetadata {
+
+ private String userId;
+ private String parentUserId;
+ private String username;
+ private String displayName;
+
+ public String getUserId() {
+ return userId;
+ }
+
+ public void setUserId(String userId) {
+ this.userId = userId;
+ }
+
+ public String getParentUserId() {
+ return parentUserId;
+ }
+
+ public void setParentUserId(String parentUserId) {
+ this.parentUserId = parentUserId;
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
+ public String getDisplayName() {
+ return displayName;
+ }
+
+ public void setDisplayName(String displayName) {
+ this.displayName = displayName;
+ }
+
+ @Override
+ public String toString() {
+ return "ConversationSenderMetadata{" +
+ "userId='" + userId + '\'' +
+ ", parentUserId='" + parentUserId + '\'' +
+ ", username='" + username + '\'' +
+ ", displayName='" + displayName + '\'' +
+ '}';
+ }
+}
diff --git a/api/src/main/java/com/messagebird/objects/conversations/ConversationStatusMessageMetadata.java b/api/src/main/java/com/messagebird/objects/conversations/ConversationStatusMessageMetadata.java
new file mode 100644
index 00000000..47a4a8d1
--- /dev/null
+++ b/api/src/main/java/com/messagebird/objects/conversations/ConversationStatusMessageMetadata.java
@@ -0,0 +1,92 @@
+package com.messagebird.objects.conversations;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+/**
+ * The {@code messageMetadata} block delivered inside status webhook payloads
+ * (e.g. {@code statusSent}, {@code statusDelivered}). Reflects the original
+ * message that triggered the status update.
+ *
+ *
This class is not produced by any SDK request — it is a standalone POJO
+ * intended for consumers who deserialize incoming webhook payloads in their
+ * own HTTP handlers. Use it via {@code ObjectMapper.readValue(body, ...)}.
+ *
+ *
Both {@code from} and {@code to} accept either a phone number or a
+ * WhatsApp Business-Scoped User ID (BSUID, e.g. "US.13491208655302741918").
+ * The BSUID is also available via {@code metadata.sender.userId}.
+ *
+ *
{@code to} echoes back the address the message was originally addressed
+ * to, so it is not a reliable source of the recipient's BSUID. That identity
+ * lives alongside this block, under {@code status.metadata.recipient} — see
+ * {@link ConversationStatusMetadata}.
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class ConversationStatusMessageMetadata {
+
+ private String id;
+ private String from;
+ private String to;
+ private String type;
+ private ConversationContent content;
+ private ConversationMessageMetadata metadata;
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getFrom() {
+ return from;
+ }
+
+ public void setFrom(String from) {
+ this.from = from;
+ }
+
+ public String getTo() {
+ return to;
+ }
+
+ public void setTo(String to) {
+ this.to = to;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+
+ public ConversationContent getContent() {
+ return content;
+ }
+
+ public void setContent(ConversationContent content) {
+ this.content = content;
+ }
+
+ public ConversationMessageMetadata getMetadata() {
+ return metadata;
+ }
+
+ public void setMetadata(ConversationMessageMetadata metadata) {
+ this.metadata = metadata;
+ }
+
+ @Override
+ public String toString() {
+ return "ConversationStatusMessageMetadata{" +
+ "id='" + id + '\'' +
+ ", from='" + from + '\'' +
+ ", to='" + to + '\'' +
+ ", type='" + type + '\'' +
+ ", content=" + content +
+ ", metadata=" + metadata +
+ '}';
+ }
+}
diff --git a/api/src/main/java/com/messagebird/objects/conversations/ConversationStatusMetadata.java b/api/src/main/java/com/messagebird/objects/conversations/ConversationStatusMetadata.java
new file mode 100644
index 00000000..0b4d5f7f
--- /dev/null
+++ b/api/src/main/java/com/messagebird/objects/conversations/ConversationStatusMetadata.java
@@ -0,0 +1,85 @@
+package com.messagebird.objects.conversations;
+
+import com.fasterxml.jackson.annotation.JsonAnyGetter;
+import com.fasterxml.jackson.annotation.JsonAnySetter;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * The {@code status.metadata} block delivered inside status webhook payloads
+ * (e.g. {@code statusSent}, {@code statusDelivered}).
+ *
+ *
This class is not produced by any SDK request — it is a standalone POJO
+ * intended for consumers who deserialize incoming webhook payloads in their own
+ * HTTP handlers. Use it via {@code ObjectMapper.readValue(body, ...)}.
+ *
+ *
Note the mixed casing of this object. {@code pricing} and
+ * {@code conversation} are near-verbatim passthroughs of Meta's own objects and
+ * so keep their snake_case keys ({@code pricing_model}, {@code category}, …);
+ * they are exposed here as raw maps rather than modelled types, because their
+ * contents track Meta's schema rather than ours. {@code recipient} is ours and
+ * follows the camelCase convention used everywhere else in the API. Any other
+ * key present on the payload — for example {@code biz_opaque_callback_data} —
+ * is collected into {@link #getAdditionalProperties()} rather than dropped.
+ *
+ *
{@code recipient} is absent from payloads for accounts that never receive
+ * BSUIDs, in which case {@link #getRecipient()} returns {@code null}.
+ */
+public class ConversationStatusMetadata {
+
+ private Map pricing;
+ private Map conversation;
+ private ConversationRecipientMetadata recipient;
+ private final Map additionalProperties = new LinkedHashMap<>();
+
+ public Map getPricing() {
+ return pricing;
+ }
+
+ public void setPricing(Map pricing) {
+ this.pricing = pricing;
+ }
+
+ public Map getConversation() {
+ return conversation;
+ }
+
+ public void setConversation(Map conversation) {
+ this.conversation = conversation;
+ }
+
+ public ConversationRecipientMetadata getRecipient() {
+ return recipient;
+ }
+
+ public void setRecipient(ConversationRecipientMetadata recipient) {
+ this.recipient = recipient;
+ }
+
+ /**
+ * Every key on the payload that has no dedicated accessor above, in the
+ * order it was encountered. Empty when the payload holds nothing else.
+ *
+ * @return the unmodelled remainder of the metadata object
+ */
+ @JsonAnyGetter
+ public Map getAdditionalProperties() {
+ return additionalProperties;
+ }
+
+ @JsonAnySetter
+ public void setAdditionalProperty(String name, Object value) {
+ additionalProperties.put(name, value);
+ }
+
+ @Override
+ public String toString() {
+ return "ConversationStatusMetadata{" +
+ "pricing=" + pricing +
+ ", conversation=" + conversation +
+ ", recipient=" + recipient +
+ ", additionalProperties=" + additionalProperties +
+ '}';
+ }
+}
diff --git a/api/src/main/java/com/messagebird/objects/conversations/MessageComponentType.java b/api/src/main/java/com/messagebird/objects/conversations/MessageComponentType.java
index cdb695cd..9f9290cf 100644
--- a/api/src/main/java/com/messagebird/objects/conversations/MessageComponentType.java
+++ b/api/src/main/java/com/messagebird/objects/conversations/MessageComponentType.java
@@ -13,7 +13,8 @@ public enum MessageComponentType {
BUTTON("button"),
CARD("card"),
CAROUSEL("carousel"),
- LIMITED_TIME_OFFER("limited_time_offer");
+ LIMITED_TIME_OFFER("limited_time_offer"),
+ COPY_CODE("copy_code");
private static final Map TYPE_MAP;
diff --git a/api/src/main/java/com/messagebird/objects/conversations/MessageParam.java b/api/src/main/java/com/messagebird/objects/conversations/MessageParam.java
index 38ae18f6..6432369f 100644
--- a/api/src/main/java/com/messagebird/objects/conversations/MessageParam.java
+++ b/api/src/main/java/com/messagebird/objects/conversations/MessageParam.java
@@ -15,6 +15,8 @@ public class MessageParam {
private Media video;
@JsonProperty("expiration_time")
private String expirationTime;
+ @JsonProperty("coupon_code")
+ private String couponCode;
public TemplateMediaType getType() {
return type;
@@ -93,6 +95,17 @@ public void setExpirationTime(String expirationTime) {
this.expirationTime = expirationTime;
}
+ public String getCouponCode() {
+ return couponCode;
+ }
+
+ public void setCouponCode(String couponCode) {
+ if (StringUtils.isBlank(couponCode)) {
+ throw new IllegalArgumentException("couponCode cannot be null or empty");
+ }
+ this.couponCode = couponCode;
+ }
+
@Override
public String toString() {
StringBuilder sb = new StringBuilder("MessageParam{");
@@ -105,6 +118,7 @@ public String toString() {
.append(", image=").append(image)
.append(", video=").append(video)
.append(", expirationTime='").append(expirationTime).append('\'')
+ .append(", couponCode='").append(couponCode).append('\'')
.append('}');
return sb.toString();
}
diff --git a/api/src/main/java/com/messagebird/objects/conversations/TemplateMediaType.java b/api/src/main/java/com/messagebird/objects/conversations/TemplateMediaType.java
index fd600ab2..580dcc97 100644
--- a/api/src/main/java/com/messagebird/objects/conversations/TemplateMediaType.java
+++ b/api/src/main/java/com/messagebird/objects/conversations/TemplateMediaType.java
@@ -15,7 +15,8 @@ public enum TemplateMediaType {
CURRENCY("currency"),
DATETIME("date_time"),
PAYLOAD("payload"),
- EXPIRATION_TIME("expiration_time");
+ EXPIRATION_TIME("expiration_time"),
+ COUPON_CODE("coupon_code");
private static final Map TYPE_MAP;
diff --git a/api/src/test/java/com/messagebird/ContactTest.java b/api/src/test/java/com/messagebird/ContactTest.java
index 2377733d..5dfdea38 100644
--- a/api/src/test/java/com/messagebird/ContactTest.java
+++ b/api/src/test/java/com/messagebird/ContactTest.java
@@ -8,6 +8,7 @@
import org.mockito.Mockito;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
+import static org.junit.Assume.assumeNotNull;
import static org.unitils.reflectionassert.ReflectionAssert.assertReflectionEquals;
/**
@@ -30,6 +31,7 @@ public class ContactTest {
@BeforeClass
public static void setUpClass() throws UnauthorizedException, GeneralException {
String accessKey = System.getProperty("messageBirdAccessKey");
+ assumeNotNull("Integration test skipped: set -DmessageBirdAccessKey to run", accessKey);
msisdn = generateMsisdn();
diff --git a/api/src/test/java/com/messagebird/ConversationMessagesTest.java b/api/src/test/java/com/messagebird/ConversationMessagesTest.java
index 81357f88..beac543a 100644
--- a/api/src/test/java/com/messagebird/ConversationMessagesTest.java
+++ b/api/src/test/java/com/messagebird/ConversationMessagesTest.java
@@ -1,5 +1,6 @@
package com.messagebird;
+import com.fasterxml.jackson.databind.ObjectMapper;
import com.messagebird.exceptions.GeneralException;
import com.messagebird.exceptions.NotFoundException;
import com.messagebird.exceptions.UnauthorizedException;
@@ -23,6 +24,19 @@ public class ConversationMessagesTest {
private static final String JSON_CONVERSATION_MESSAGE_TEXT = "{\"id\": \"mesid\",\"conversationId\": \"convid\",\"channelId\": \"chanid\",\"status\": \"received\",\"type\": \"text\",\"direction\": \"received\",\"content\": {\"text\": \"Hello\"},\"createdDatetime\": \"2018-08-29T11:49:16Z\",\"updatedDatetime\": \"2018-08-29T11:49:16Z\"}";
private static final String JSON_CONVERSATION_MESSAGE_VIDEO = "{\"id\": \"mesid\",\"conversationId\": \"convid\",\"channelId\": \"chanid\",\"status\": \"received\",\"type\": \"video\",\"direction\": \"received\",\"content\": {\"video\": { \"url\": \"https://example.com/video.mp4\" } },\"createdDatetime\": \"2018-08-29T11:49:16Z\",\"updatedDatetime\": \"2018-08-29T11:49:16Z\"}";
private static final String JSON_CONVERSATION_SEND_MESSAGE_RESPONSE = "{\"id\":\"mesid\",\"status\":\"accepted\",\"fallback\":{\"id\":\"mesid\"}}";
+ private static final String JSON_CONVERSATION_MESSAGE_BSUID = "{\"id\": \"mesid\",\"conversationId\": \"convid\",\"channelId\": \"chanid\",\"status\": \"received\",\"type\": \"text\",\"direction\": \"received\",\"content\": {\"text\": \"Hello\"},\"metadata\": {\"sender\": {\"displayName\": \"Alice\",\"username\": \"alice_shop\",\"userId\": \"US.13491208655302741918\",\"parentUserId\": \"US.ENT.11815799212886844830\"},\"receivedAt\": \"2025-04-15T16:00:00Z\"},\"createdDatetime\": \"2025-04-15T16:00:00Z\",\"updatedDatetime\": \"2025-04-15T16:00:00Z\"}";
+ private static final String JSON_STATUS_MESSAGE_METADATA = "{\"id\": \"e5f6a7b8-c9d0-1234-ef01-23456789abcd\",\"from\": \"15551234567\",\"to\": \"US.13491208655302741918\",\"type\": \"text\",\"content\": {\"text\": \"Hello! Your order has been shipped.\"},\"metadata\": {\"sender\": {\"userId\": \"US.13491208655302741918\"},\"receivedAt\": \"0001-01-01T00:00:00Z\"}}";
+
+ private static final String JSON_STATUS_METADATA_WITH_RECIPIENT = "{\"pricing\": {\"billable\": true,\"pricing_model\": \"CBP\",\"category\": \"utility\"},\"conversation\": {\"id\": \"a1b2c3d4\",\"origin\": {\"type\": \"utility\"}},\"biz_opaque_callback_data\": \"order-1234\",\"recipient\": {\"userId\": \"US.13491208655302741918\",\"parentUserId\": \"US.ENT.11815799212886844830\"}}";
+ private static final String JSON_STATUS_METADATA_WITHOUT_RECIPIENT = "{\"pricing\": {\"billable\": true,\"pricing_model\": \"CBP\",\"category\": \"utility\"},\"conversation\": {\"id\": \"a1b2c3d4\",\"origin\": {\"type\": \"utility\"}}}";
+ private static final String JSON_STATUS_METADATA_PARENT_ONLY = "{\"recipient\": {\"parentUserId\": \"US.ENT.11815799212886844830\"}}";
+
+ /**
+ * The same payload as JSON_STATUS_MESSAGE_METADATA with an unrecognised key
+ * added at every nesting level, standing in for fields the platform adds
+ * after this SDK version ships.
+ */
+ private static final String JSON_STATUS_MESSAGE_METADATA_UNKNOWN_FIELDS = "{\"id\": \"e5f6a7b8-c9d0-1234-ef01-23456789abcd\",\"from\": \"15551234567\",\"to\": \"US.13491208655302741918\",\"type\": \"text\",\"futureTopLevelField\": \"ignored\",\"content\": {\"text\": \"Hello! Your order has been shipped.\",\"futureContentField\": \"ignored\"},\"metadata\": {\"sender\": {\"userId\": \"US.13491208655302741918\",\"futureSenderField\": \"ignored\"},\"receivedAt\": \"0001-01-01T00:00:00Z\",\"futureMetadataField\": \"ignored\"}}";
/**
* Epsilon to use when checking two latitudes or longitudes for equality.
@@ -183,6 +197,96 @@ public void testViewConversationMessageLocation() throws GeneralException, NotFo
assertEquals(4.911627, location.getLongitude(), EPSILON_LOCATION_EQUALITY);
}
+ @Test
+ public void testViewConversationMessageWithBsuidMetadata() throws GeneralException, NotFoundException, UnauthorizedException {
+ MessageBirdService messageBirdService = SpyService
+ .expects("GET", "messages/mesid")
+ .withConversationsAPIBaseURL()
+ .andReturns(new APIResponse(JSON_CONVERSATION_MESSAGE_BSUID));
+ MessageBirdClient messageBirdClient = new MessageBirdClient(messageBirdService);
+
+ ConversationMessage message = messageBirdClient.viewConversationMessage("mesid");
+
+ ConversationMessageMetadata metadata = message.getMetadata();
+ assertNotNull(metadata);
+ assertNotNull(metadata.getReceivedAt());
+ ConversationSenderMetadata sender = metadata.getSender();
+ assertEquals("Alice", sender.getDisplayName());
+ assertEquals("alice_shop", sender.getUsername());
+ assertEquals("US.13491208655302741918", sender.getUserId());
+ assertEquals("US.ENT.11815799212886844830", sender.getParentUserId());
+ }
+
+ @Test
+ public void testStatusMessageMetadataToleratesUnknownFields() throws Exception {
+ // A plain mapper, and unknown keys at every level: webhook payload POJOs
+ // must not force consumers to disable FAIL_ON_UNKNOWN_PROPERTIES, and
+ // must survive fields the platform adds after this version ships.
+ ConversationStatusMessageMetadata md = new ObjectMapper().readValue(
+ JSON_STATUS_MESSAGE_METADATA_UNKNOWN_FIELDS, ConversationStatusMessageMetadata.class);
+
+ assertEquals("e5f6a7b8-c9d0-1234-ef01-23456789abcd", md.getId());
+ assertEquals("Hello! Your order has been shipped.", md.getContent().getText());
+ assertEquals("US.13491208655302741918", md.getMetadata().getSender().getUserId());
+ assertNotNull(md.getMetadata().getReceivedAt());
+ }
+
+ @Test
+ public void testStatusMetadataDeserializesRecipient() throws Exception {
+ // A plain mapper: these payload POJOs must not require the caller to
+ // disable FAIL_ON_UNKNOWN_PROPERTIES.
+ ConversationStatusMetadata metadata = new ObjectMapper().readValue(
+ JSON_STATUS_METADATA_WITH_RECIPIENT, ConversationStatusMetadata.class);
+
+ ConversationRecipientMetadata recipient = metadata.getRecipient();
+ assertNotNull(recipient);
+ assertEquals("US.13491208655302741918", recipient.getUserId());
+ assertEquals("US.ENT.11815799212886844830", recipient.getParentUserId());
+
+ // Meta's own objects pass through untouched, snake_case keys and all.
+ assertEquals("CBP", metadata.getPricing().get("pricing_model"));
+ assertEquals("a1b2c3d4", metadata.getConversation().get("id"));
+
+ // Anything else on the payload is kept rather than dropped.
+ assertEquals("order-1234", metadata.getAdditionalProperties().get("biz_opaque_callback_data"));
+ }
+
+ @Test
+ public void testStatusMetadataWithoutRecipientIsNull() throws Exception {
+ ConversationStatusMetadata metadata = new ObjectMapper().readValue(
+ JSON_STATUS_METADATA_WITHOUT_RECIPIENT, ConversationStatusMetadata.class);
+
+ // Accounts that never receive BSUIDs get payloads with no recipient key
+ // at all — the rest of the metadata must still parse.
+ assertNull(metadata.getRecipient());
+ assertEquals("CBP", metadata.getPricing().get("pricing_model"));
+ assertTrue(metadata.getAdditionalProperties().isEmpty());
+ }
+
+ @Test
+ public void testStatusMetadataRecipientWithParentOnly() throws Exception {
+ ConversationStatusMetadata metadata = new ObjectMapper().readValue(
+ JSON_STATUS_METADATA_PARENT_ONLY, ConversationStatusMetadata.class);
+
+ ConversationRecipientMetadata recipient = metadata.getRecipient();
+ assertNotNull(recipient);
+ assertNull(recipient.getUserId());
+ assertEquals("US.ENT.11815799212886844830", recipient.getParentUserId());
+ }
+
+ @Test
+ public void testStatusMessageMetadataDeserializes() throws Exception {
+ ConversationStatusMessageMetadata md = new ObjectMapper().readValue(
+ JSON_STATUS_MESSAGE_METADATA, ConversationStatusMessageMetadata.class);
+
+ assertEquals("e5f6a7b8-c9d0-1234-ef01-23456789abcd", md.getId());
+ assertEquals("15551234567", md.getFrom());
+ assertEquals("US.13491208655302741918", md.getTo());
+ assertEquals("text", md.getType());
+ assertEquals("Hello! Your order has been shipped.", md.getContent().getText());
+ assertEquals("US.13491208655302741918", md.getMetadata().getSender().getUserId());
+ }
+
@Test
public void testViewConversationMessageText() throws GeneralException, NotFoundException, UnauthorizedException {
MessageBirdService messageBirdService = SpyService
diff --git a/api/src/test/java/com/messagebird/MessageBirdClientTest.java b/api/src/test/java/com/messagebird/MessageBirdClientTest.java
index ee2dc890..b11c2f54 100644
--- a/api/src/test/java/com/messagebird/MessageBirdClientTest.java
+++ b/api/src/test/java/com/messagebird/MessageBirdClientTest.java
@@ -16,6 +16,7 @@
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.Mockito;
+import static org.junit.Assume.assumeNotNull;
import java.math.BigInteger;
import java.util.Collections;
@@ -44,7 +45,10 @@ public class MessageBirdClientTest {
@BeforeClass
public static void setUpClass() {
messageBirdAccessKey = System.getProperty("messageBirdAccessKey");
- messageBirdMSISDN = new BigInteger(System.getProperty("messageBirdMSISDN"));
+ String msisdn = System.getProperty("messageBirdMSISDN");
+ assumeNotNull("Integration test skipped: set -DmessageBirdAccessKey and -DmessageBirdMSISDN to run",
+ messageBirdAccessKey, msisdn);
+ messageBirdMSISDN = new BigInteger(msisdn);
}
@Before
diff --git a/api/src/test/java/com/messagebird/objects/conversations/MessageParamTest.java b/api/src/test/java/com/messagebird/objects/conversations/MessageParamTest.java
index 6ddabad7..f1147d2f 100644
--- a/api/src/test/java/com/messagebird/objects/conversations/MessageParamTest.java
+++ b/api/src/test/java/com/messagebird/objects/conversations/MessageParamTest.java
@@ -11,7 +11,7 @@ public void testMessageParamToString() {
param.setText("Sample text");
param.setPayload("Sample payload");
- String expected = "MessageParam{type=image, text='Sample text', payload='Sample payload', currency=null, dateTime='null', document=null, image=null, video=null, expirationTime='null'}";
+ String expected = "MessageParam{type=image, text='Sample text', payload='Sample payload', currency=null, dateTime='null', document=null, image=null, video=null, expirationTime='null', couponCode='null'}";
assertEquals(expected, param.toString());
}
diff --git a/examples/pom.xml b/examples/pom.xml
index 4a98866a..cb9b7489 100644
--- a/examples/pom.xml
+++ b/examples/pom.xml
@@ -6,7 +6,7 @@
com.messagebird
examples
- 6.2.2
+ 6.4.0
@@ -20,7 +20,7 @@
com.messagebird
messagebird-api
- 6.2.2
+ 6.4.0
compile
diff --git a/examples/src/main/java/ExampleConversationSendHSMCopyCodeTemplate.java b/examples/src/main/java/ExampleConversationSendHSMCopyCodeTemplate.java
new file mode 100644
index 00000000..4d5dcd3f
--- /dev/null
+++ b/examples/src/main/java/ExampleConversationSendHSMCopyCodeTemplate.java
@@ -0,0 +1,92 @@
+import com.messagebird.MessageBirdClient;
+import com.messagebird.MessageBirdService;
+import com.messagebird.MessageBirdServiceImpl;
+import com.messagebird.exceptions.GeneralException;
+import com.messagebird.exceptions.UnauthorizedException;
+import com.messagebird.objects.conversations.*;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class ExampleConversationSendHSMCopyCodeTemplate {
+
+ public static void main(String[] args) {
+ if (args.length < 6) {
+ System.out.println("Please at least specify your access key, the channel id and destination address.\n" +
+ "Usage : java -jar test_accesskey(Required) channel_id(Required) from(Required) destination(Required) templateName(Required) namespace(Required) couponCodeInput(Required)");
+ return;
+ }
+
+ final String accessKey = args[0];
+ final String from = args[1];
+ final String destination = args[2];
+ final String templateName = args[3];
+ final String namespace = args[4];
+ final String couponCodeInput = args[5];
+
+ //First create your service object
+ final MessageBirdService wsr = new MessageBirdServiceImpl(accessKey);
+ //Add the service to the client
+ final MessageBirdClient messageBirdClient = new MessageBirdClient(wsr);
+
+ ConversationContent conversationContent = new ConversationContent();
+ ConversationContentHsm conversationContentHsm = new ConversationContentHsm();
+ conversationContentHsm.setNamespace(namespace);
+ conversationContentHsm.setTemplateName(templateName);
+ ConversationHsmLanguage language = new ConversationHsmLanguage();
+ language.setCode("en");
+ conversationContentHsm.setLanguage(language);
+ List messageComponents = new ArrayList<>();
+
+ // Add LTO component
+ MessageComponent messageCopyCodeComponent = new MessageComponent();
+ messageCopyCodeComponent.setType(MessageComponentType.BUTTON);
+ messageCopyCodeComponent.setSub_type(MessageComponentType.COPY_CODE.toString());
+ List messageCCParams = new ArrayList<>();
+
+ MessageParam couponCodeParam = new MessageParam();
+ couponCodeParam.setType(TemplateMediaType.COUPON_CODE);
+ couponCodeParam.setCouponCode(couponCodeInput);
+ messageCCParams.add(couponCodeParam);
+
+ messageCopyCodeComponent.setParameters(messageCCParams);
+
+ // Add body component
+ MessageComponent messageBodyComponent = new MessageComponent();
+ messageBodyComponent.setType(MessageComponentType.BODY);
+ List messageBodyParams = new ArrayList<>();
+
+ MessageParam text = new MessageParam();
+ text.setType(TemplateMediaType.TEXT);
+ text.setText("Bob");
+ messageBodyParams.add(text);
+
+ messageBodyComponent.setParameters(messageBodyParams);
+
+ messageComponents.add(messageCopyCodeComponent);
+ messageComponents.add(messageBodyComponent);
+ conversationContentHsm.setComponents(messageComponents);
+ conversationContent.setHsm(conversationContentHsm);
+ ConversationSendRequest request = new ConversationSendRequest(
+ destination,
+ ConversationContentType.HSM,
+ conversationContent,
+ from,
+ "",
+ null,
+ null,
+ null);
+
+ try {
+ System.out.println(request.toString());
+ ConversationSendResponse sendResponse = messageBirdClient.sendMessage(request);
+ System.out.println(sendResponse.toString());
+
+ } catch (UnauthorizedException e) {
+ System.err.println("Authorization failed. Please check your access key: " + e.getMessage());
+ } catch (GeneralException e) {
+ System.err.println("An error occurred while sending the message: " + e.getMessage());
+ }
+ }
+
+}