Skip to content

Commit 1efde32

Browse files
committed
Add b2b delivery experience code recipe
1 parent 2ffd61b commit 1efde32

File tree

10 files changed

+770
-0
lines changed

10 files changed

+770
-0
lines changed

code-recipes/java/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ npmInstall.dependsOn("processTestResources")
4141

4242
dependencies {
4343
implementation 'software.amazon.spapi:spapi-sdk:1.6.0'
44+
implementation 'org.threeten:threetenbp:1.6.5'
4445
testImplementation 'com.fasterxml.jackson.jr:jackson-jr-all:2.20.0'
4546
testImplementation 'org.junit.jupiter:junit-jupiter:5.13.4'
4647
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
Lines changed: 328 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,328 @@
1+
package b2bdeliveryexperience;
2+
3+
import java.time.DayOfWeek;
4+
import java.time.LocalDate;
5+
import java.util.List;
6+
import java.util.stream.Collectors;
7+
8+
import com.amazon.SellingPartnerAPIAA.LWAException;
9+
10+
import org.threeten.bp.OffsetDateTime;
11+
import software.amazon.spapi.ApiException;
12+
import software.amazon.spapi.api.orders.v0.OrdersV0Api;
13+
import software.amazon.spapi.models.orders.v0.Address.AddressTypeEnum;
14+
import software.amazon.spapi.models.orders.v0.ConfirmShipmentOrderItem;
15+
import software.amazon.spapi.models.orders.v0.ConfirmShipmentOrderItemsList;
16+
import software.amazon.spapi.models.orders.v0.ConfirmShipmentRequest;
17+
import software.amazon.spapi.models.orders.v0.GetOrderAddressResponse;
18+
import software.amazon.spapi.models.orders.v0.GetOrderBuyerInfoResponse;
19+
import software.amazon.spapi.models.orders.v0.GetOrderItemsResponse;
20+
import software.amazon.spapi.models.orders.v0.GetOrderResponse;
21+
import software.amazon.spapi.models.orders.v0.Order;
22+
import software.amazon.spapi.models.orders.v0.OrderAddress;
23+
import software.amazon.spapi.models.orders.v0.OrderBuyerInfo;
24+
import software.amazon.spapi.models.orders.v0.OrderItem;
25+
import software.amazon.spapi.models.orders.v0.PackageDetail;
26+
import util.Constants;
27+
import util.Recipe;
28+
29+
import static software.amazon.spapi.models.orders.v0.Address.AddressTypeEnum.COMMERCIAL;
30+
31+
/**
32+
* Amazon Business Delivery Experience is a six-step process:
33+
* 1) Get order details and check if it's a business order
34+
* 2) Retrieve purchase order number (RDT required)
35+
* 3) Get order address and verify it's commercial (RDT required)
36+
* 4) Filter carrier options to exclude weekend deliveries (for commercial addresses)
37+
* 5) Generate shipping label with PO number
38+
* 6) Confirm shipment with selected carrier
39+
* NOTE: Methods that access PII (buyer info, address) require a Restricted Data Token (RDT).
40+
*/
41+
public class BusinessDeliveryExperienceRecipe extends Recipe {
42+
43+
private final OrdersV0Api ordersApi = new OrdersV0Api.Builder()
44+
.lwaAuthorizationCredentials(lwaCredentials)
45+
.endpoint(Constants.BACKEND_URL)
46+
.build();
47+
48+
@Override
49+
protected void start() {
50+
final String orderId = "123-4567890-1234567"; // Sample order ID
51+
52+
Order order = getOrder(orderId);
53+
if (!isBusinessOrder(order)) {
54+
System.out.println("Not a business order; skipping business delivery flow.");
55+
return;
56+
}
57+
58+
// RDT must be obtained before calling buyer info or address endpoints.
59+
String poNumber = getPurchaseOrderNumber(orderId);
60+
List<OrderItem> orderItems = getOrderItems(orderId);
61+
OrderAddress orderAddress = getOrderAddress(orderId);
62+
63+
List<CarrierOption> carriers = getCarrierOptions();
64+
65+
// Filter weekend deliveries only for commercial addresses
66+
if (isCommercialAddress(orderAddress)) {
67+
carriers = filterWeekendDeliveries(carriers);
68+
}
69+
70+
CarrierOption selectedCarrier = selectCarrier(carriers);
71+
ShippingLabel label = generateShippingLabel(orderId, selectedCarrier, poNumber);
72+
confirmShipment(order, orderItems, selectedCarrier);
73+
}
74+
75+
/**
76+
* Checks if an order is a business order.
77+
*/
78+
private boolean isBusinessOrder(Order order) {
79+
return order != null && Boolean.TRUE.equals(order.isIsBusinessOrder());
80+
}
81+
82+
/**
83+
* Gets order details - no Restricted Data Token required.
84+
*/
85+
private Order getOrder(String orderId) {
86+
try {
87+
GetOrderResponse response = ordersApi.getOrder(orderId);
88+
return response.getPayload();
89+
} catch (ApiException e) {
90+
throw new RuntimeException("Unsuccessful response from Orders API", e);
91+
} catch (LWAException e) {
92+
throw new RuntimeException("Authentication error", e);
93+
}
94+
}
95+
96+
/**
97+
* Gets Purchase Order Number - REQUIRES Restricted Data Token (RDT) for PII access.
98+
* Obtain an RDT beforehand using the createRestrictedDataToken API.
99+
*/
100+
private String getPurchaseOrderNumber(String orderId) {
101+
try {
102+
GetOrderBuyerInfoResponse buyerInfoResponse = ordersApi.getOrderBuyerInfo(orderId);
103+
OrderBuyerInfo buyerInfo = (buyerInfoResponse != null) ? buyerInfoResponse.getPayload() : null;
104+
return (buyerInfo != null) ? buyerInfo.getPurchaseOrderNumber() : null;
105+
} catch (ApiException e) {
106+
throw new RuntimeException("Unsuccessful response from Orders API", e);
107+
} catch (LWAException e) {
108+
throw new RuntimeException("Authentication error", e);
109+
}
110+
}
111+
112+
/**
113+
* Gets order items - no Restricted Data Token required.
114+
*/
115+
private List<OrderItem> getOrderItems(String orderId) {
116+
try {
117+
GetOrderItemsResponse response = ordersApi.getOrderItems(orderId, null);
118+
return response.getPayload().getOrderItems();
119+
} catch (ApiException e) {
120+
throw new RuntimeException("Unsuccessful response from Orders API", e);
121+
} catch (LWAException e) {
122+
throw new RuntimeException("Authentication error", e);
123+
}
124+
}
125+
126+
/**
127+
* Gets order address - REQUIRES Restricted Data Token (RDT) for PII access.
128+
* Obtain an RDT beforehand using the createRestrictedDataToken API.
129+
*/
130+
private OrderAddress getOrderAddress(String orderId) {
131+
try {
132+
GetOrderAddressResponse response = ordersApi.getOrderAddress(orderId);
133+
return response.getPayload(); // payload contains shippingAddress
134+
} catch (ApiException e) {
135+
throw new RuntimeException("Unsuccessful response from Orders API", e);
136+
} catch (LWAException e) {
137+
throw new RuntimeException("Authentication error", e);
138+
}
139+
}
140+
141+
// ---------- Helper / mock logic ----------
142+
143+
/**
144+
* True if the shipping address type is "Commercial" (case-insensitive).
145+
*/
146+
private boolean isCommercialAddress(OrderAddress orderAddress) {
147+
if (orderAddress == null || orderAddress.getShippingAddress() == null) return false;
148+
AddressTypeEnum addressType = orderAddress.getShippingAddress().getAddressType();
149+
return COMMERCIAL.equals(addressType);
150+
}
151+
152+
/**
153+
* MOCK: Get available carrier options (replace with real carrier/shipping API integration).
154+
*/
155+
private List<CarrierOption> getCarrierOptions() {
156+
return List.of(
157+
createCarrierOption("UPS", LocalDate.now().plusDays(1)),
158+
createCarrierOption("FedEx", LocalDate.now().plusDays(2)),
159+
createCarrierOption("DHL", LocalDate.now().plusDays(6)) // Saturday
160+
);
161+
}
162+
163+
private CarrierOption createCarrierOption(String carrier, LocalDate deliveryDate) {
164+
CarrierOption option = new CarrierOption();
165+
option.setCarrierName(carrier);
166+
option.setEstimatedDeliveryDate(deliveryDate.toString());
167+
return option;
168+
}
169+
170+
/**
171+
* MOCK METHOD: Filters out carriers that deliver on weekends
172+
* In real implementation, this would be part of carrier selection logic
173+
*/
174+
private List<CarrierOption> filterWeekendDeliveries(List<CarrierOption> carriers) {
175+
return carriers.stream()
176+
.filter(c -> {
177+
LocalDate date = LocalDate.parse(c.getEstimatedDeliveryDate());
178+
DayOfWeek dow = date.getDayOfWeek();
179+
return dow != DayOfWeek.SATURDAY && dow != DayOfWeek.SUNDAY;
180+
})
181+
.collect(Collectors.toList());
182+
}
183+
184+
/**
185+
* Simple selection logic; pick first available.
186+
*/
187+
private CarrierOption selectCarrier(List<CarrierOption> carriers) {
188+
return (carriers == null || carriers.isEmpty()) ? null : carriers.get(0);
189+
}
190+
191+
/**
192+
* MOCK METHOD: Generates shipping label with purchase order number
193+
* In real implementation, this would integrate with SP-API Shipping API or carrier APIs
194+
*/
195+
private ShippingLabel generateShippingLabel(String orderId, CarrierOption carrier, String poNumber) {
196+
if (carrier == null) {
197+
System.out.println("No carrier available for label generation");
198+
return null;
199+
}
200+
201+
ShippingLabel label = new ShippingLabel();
202+
label.setLabelId("LBL-" + orderId + "-" + System.currentTimeMillis());
203+
label.setCarrierName(carrier.getCarrierName());
204+
label.setTrackingNumber("1Z999AA1234567890");
205+
label.setLabelFormat("PDF");
206+
label.setLabelUrl("https://mock-label-url.com/" + label.getLabelId() + ".pdf");
207+
label.setPurchaseOrderNumber(poNumber);
208+
209+
System.out.println("Shipping label generated: " + label.getLabelId() +
210+
(poNumber != null ? " with PO: " + poNumber : ""));
211+
return label;
212+
}
213+
214+
/**
215+
* Confirm shipment in Orders API.
216+
*/
217+
private void confirmShipment(Order order, List<OrderItem> orderItems, CarrierOption carrier) {
218+
if (order == null || carrier == null || orderItems == null || orderItems.isEmpty()) {
219+
System.out.println("Missing data; cannot confirm shipment.");
220+
return;
221+
}
222+
223+
try {
224+
ConfirmShipmentOrderItemsList confirmItems = new ConfirmShipmentOrderItemsList();
225+
orderItems.stream()
226+
.map(item -> new ConfirmShipmentOrderItem()
227+
.orderItemId(item.getOrderItemId())
228+
.quantity(item.getQuantityOrdered()))
229+
.forEach(confirmItems::add);
230+
231+
ConfirmShipmentRequest request = new ConfirmShipmentRequest()
232+
.marketplaceId(order.getMarketplaceId())
233+
.packageDetail(new PackageDetail()
234+
.packageReferenceId("PKG001")
235+
.carrierCode(carrier.getCarrierName())
236+
.trackingNumber("1Z999AA1234567890")
237+
.orderItems(confirmItems)
238+
.shipDate(OffsetDateTime.now()));
239+
240+
ordersApi.confirmShipment(request, order.getAmazonOrderId());
241+
System.out.println("Shipment confirmed with carrier: " + carrier.getCarrierName());
242+
} catch (ApiException e) {
243+
throw new RuntimeException("Failed to confirm shipment", e);
244+
} catch (LWAException e) {
245+
throw new RuntimeException("Authentication error", e);
246+
}
247+
}
248+
249+
// ---------- Mock DTOs (replace with real models if available) ----------
250+
251+
private static class CarrierOption {
252+
private String carrierName;
253+
private String estimatedDeliveryDate;
254+
255+
public String getCarrierName() {
256+
return carrierName;
257+
}
258+
259+
public void setCarrierName(String carrierName) {
260+
this.carrierName = carrierName;
261+
}
262+
263+
public String getEstimatedDeliveryDate() {
264+
return estimatedDeliveryDate;
265+
}
266+
267+
public void setEstimatedDeliveryDate(String estimatedDeliveryDate) {
268+
this.estimatedDeliveryDate = estimatedDeliveryDate;
269+
}
270+
}
271+
272+
private static class ShippingLabel {
273+
private String labelId;
274+
private String carrierName;
275+
private String trackingNumber;
276+
private String labelFormat;
277+
private String labelUrl;
278+
private String purchaseOrderNumber;
279+
280+
public String getLabelId() {
281+
return labelId;
282+
}
283+
284+
public void setLabelId(String labelId) {
285+
this.labelId = labelId;
286+
}
287+
288+
public String getCarrierName() {
289+
return carrierName;
290+
}
291+
292+
public void setCarrierName(String carrierName) {
293+
this.carrierName = carrierName;
294+
}
295+
296+
public String getTrackingNumber() {
297+
return trackingNumber;
298+
}
299+
300+
public void setTrackingNumber(String trackingNumber) {
301+
this.trackingNumber = trackingNumber;
302+
}
303+
304+
public String getLabelFormat() {
305+
return labelFormat;
306+
}
307+
308+
public void setLabelFormat(String labelFormat) {
309+
this.labelFormat = labelFormat;
310+
}
311+
312+
public String getLabelUrl() {
313+
return labelUrl;
314+
}
315+
316+
public void setLabelUrl(String labelUrl) {
317+
this.labelUrl = labelUrl;
318+
}
319+
320+
public String getPurchaseOrderNumber() {
321+
return purchaseOrderNumber;
322+
}
323+
324+
public void setPurchaseOrderNumber(String purchaseOrderNumber) {
325+
this.purchaseOrderNumber = purchaseOrderNumber;
326+
}
327+
}
328+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package b2bdeliveryexperience;
2+
3+
import util.RecipeTest;
4+
5+
import java.util.List;
6+
7+
public class BusinessDeliveryExperienceRecipeTest extends RecipeTest {
8+
9+
protected BusinessDeliveryExperienceRecipeTest() {
10+
super(
11+
new BusinessDeliveryExperienceRecipe(),
12+
List.of(
13+
"orders-getOrder",
14+
"orders-getOrderBuyerInfo",
15+
"orders-getOrderItems",
16+
"orders-getOrderAddress",
17+
"orders-confirmShipment",
18+
""
19+
)
20+
);
21+
}
22+
}

0 commit comments

Comments
 (0)