Part 3 of 4: Advanced Integrations for Seamless Customer Journeys
In Part 1 and Part 2, we mastered reading, creating, and updating product data. Now it’s time to build something truly transformative – systems that create seamless customer experiences across all touchpoints.
Today, we’ll solve two critical business problems:
- The Fragmented Loyalty Problem: Mrs. Sharma’s points are scattered across channels
- The Order Processing Bottleneck: Corporate orders take hours to process manually
The Loyalty Points Disaster
The Problem
Mrs. Sharma was one of Rajesh’s most loyal customers. She purchased ₹5,000 worth of gifts from the website but her loyalty points didn’t appear in the shop’s system. When she visited the physical store the next day, staff couldn’t see her online purchase points.
The problem was clear: online and offline loyalty programs were disconnected. Customers were frustrated. Staff was confused. Rajesh was losing the goodwill he’d spent years building.
The Vision
- What Mrs. Sharma expected (and deserved):
- Earn points whether shopping online or in-store
- Check her points balance anywhere
- Redeem points across all channels
- See her complete purchase history
- Automatic tier upgrades (Silver → Gold → Platinum)
Building the Customer Loyalty System
Step 1: Creating the Loyalty Table
First, Rohan designed a custom table to store loyalty data:
al
table 50101 "Mehta Customer Loyalty"
{
DataClassification = CustomerContent;
Caption = 'Customer Loyalty';
fields
{
field(1; "Customer No."; Code[20])
{
Caption = 'Customer No.';
DataClassification = CustomerContent;
TableRelation = Customer."No.";
}
field(2; "Customer Name"; Text[100])
{
Caption = 'Customer Name';
DataClassification = CustomerContent;
FieldClass = FlowField;
CalcFormula = lookup(Customer.Name where("No." = field("Customer No.")));
}
field(3; "Total Points"; Integer)
{
Caption = 'Total Points';
DataClassification = CustomerContent;
Editable = false;
}
field(4; "Points Earned"; Integer)
{
Caption = 'Points Earned';
DataClassification = CustomerContent;
}
field(5; "Points Redeemed"; Integer)
{
Caption = 'Points Redeemed';
DataClassification = CustomerContent;
}
field(6; "Points This Year"; Integer)
{
Caption = 'Points This Year';
DataClassification = CustomerContent;
}
field(7; "Membership Level"; Enum "Mehta Membership Level")
{
Caption = 'Membership Level';
DataClassification = CustomerContent;
}
field(8; "Member Since"; Date)
{
Caption = 'Member Since';
DataClassification = CustomerContent;
}
field(9; "Last Transaction Date"; Date)
{
Caption = 'Last Transaction Date';
DataClassification = CustomerContent;
}
field(10; SystemId; Guid)
{
Caption = 'System ID';
DataClassification = SystemMetadata;
}
field(11; SystemModifiedAt; DateTime)
{
Caption = 'System Modified At';
DataClassification = SystemMetadata;
}
}
keys
{
key(PK; "Customer No.")
{
Clustered = true;
}
key(Points; "Total Points")
{
}
}
trigger OnInsert()
begin
if "Member Since" = 0D then
"Member Since" := Today;
UpdateMembershipLevel();
end;
trigger OnModify()
begin
"Total Points" := "Points Earned" - "Points Redeemed";
UpdateMembershipLevel();
"Last Transaction Date" := Today;
end;
local procedure UpdateMembershipLevel()
begin
case "Total Points" of
0..999:
"Membership Level" := "Membership Level"::Silver;
1000..4999:
"Membership Level" := "Membership Level"::Gold;
else
"Membership Level" := "Membership Level"::Platinum;
end;
end;
}
Step 2: Creating the Membership Level Enum
al
Enum 50100 "Mehta Membership Level"
{
Extensible = true;
value(0; Silver)
{
Caption = 'Silver';
}
value(1; Gold)
{
Caption = 'Gold';
}
value(2; Platinum)
{
Caption = 'Platinum';
}
}
Key Features of This Design:
- Automatic Calculations: Total points are automatically calculated when points are earned or redeemed
- Tier Management: Membership level updates automatically based on total points:
- 0-999 points: Silver
- 1000-4999 points: Gold
- 5000+ points: Platinum
- Audit Trail: System tracks when customers joined and their last transaction
- Flow Field: Customer name is pulled from the Customer table automatically
Step 3: Creating the Loyalty API
al
page 50101 "Mehta Customer Loyalty API"
{
PageType = API;
APIPublisher = 'mehtagifts';
APIGroup = 'loyalty';
APIVersion = 'v1.0';
EntityName = 'customerLoyalty';
EntitySetName = 'customerLoyalties';
SourceTable = "Mehta Customer Loyalty";
DelayedInsert = true;
ODataKeyFields = SystemId;
layout
{
area(content)
{
repeater(Group)
{
field(id; Rec.SystemId)
{
Caption = 'ID';
Editable = false;
}
field(customerNo; Rec."Customer No.")
{
Caption = 'Customer Number';
}
field(customerName; Rec."Customer Name")
{
Caption = 'Customer Name';
Editable = false;
}
field(totalPoints; Rec."Total Points")
{
Caption = 'Total Points';
Editable = false;
}
field(pointsEarned; Rec."Points Earned")
{
Caption = 'Points Earned';
}
field(pointsRedeemed; Rec."Points Redeemed")
{
Caption = 'Points Redeemed';
}
field(pointsThisYear; Rec."Points This Year")
{
Caption = 'Points This Year';
}
field(membershipLevel; Rec."Membership Level")
{
Caption = 'Membership Level';
}
field(memberSince; Rec."Member Since")
{
Caption = 'Member Since';
}
field(lastTransactionDate; Rec."Last Transaction Date")
{
Caption = 'Last Transaction';
Editable = false;
}
field(lastModifiedDateTime; Rec.SystemModifiedAt)
{
Caption = 'Last Modified';
Editable = false;
}
}
}
}
}
Using the Loyalty API
Getting Customer Loyalty Information
Request:
http
GET /api/mehtagifts/loyalty/v1.0/companies(company-id)/customerLoyalties?$filter=customerNo eq 'CUST0123'
Authorization: Bearer {access-token}
Response:
json
{
"value": [
{
"id": "8c9d0e1f-2a3b-4c5d-6e7f-8a9b0c1d2e3f",
"customerNo": "CUST0123",
"customerName": "Mrs. Sharma",
"totalPoints": 800,
"pointsEarned": 800,
"pointsRedeemed": 0,
"pointsThisYear": 800,
"membershipLevel": "Gold",
"memberSince": "2024-01-15",
"lastTransactionDate": "2024-10-20",
"lastModifiedDateTime": "2024-10-20T18:30:00Z"
}
]
}
Adding Points from Online Purchase
When Mrs. Sharma completes a ₹5,000 online purchase (earning 500 points):
Request:
http
PATCH /api/mehtagifts/loyalty/v1.0/companies(company-id)/customerLoyalties(8c9d0e1f-2a3b-4c5d-6e7f-8a9b0c1d2e3f)
Content-Type: application/json
Authorization: Bearer {access-token}
If-Match: *
{
"pointsEarned": 1300
}
The system automatically:
- Calculates total points (1300 earned – 0 redeemed = 1300 total)
- Upgrades membership level to Gold (1000-4999 points range)
- Updates last transaction date to today
Redeeming Points
When Mrs. Sharma uses 500 points for a discount:
Request:
http
PATCH /api/mehtagifts/loyalty/v1.0/companies(company-id)/customerLoyalties(8c9d0e1f-2a3b-4c5d-6e7f-8a9b0c1d2e3f)
Content-Type: application/json
Authorization: Bearer {access-token}
If-Match: *
{
"pointsRedeemed": 500
}
Response:
json
{
"id": "8c9d0e1f-2a3b-4c5d-6e7f-8a9b0c1d2e3f",
"customerNo": "CUST0123",
"customerName": "Mrs. Sharma",
"totalPoints": 800,
"pointsEarned": 1300,
"pointsRedeemed": 500,
"membershipLevel": "Gold",
"lastTransactionDate": "2024-10-22"
}
Mrs. Sharma now has 800 points remaining (1300 – 500 = 800).
The Sales Order Challenge
The Problem
A corporate client wanted to order 100 customized gift hampers through the website. In the old system:
- Website order created a record in the website database
- Staff manually entered the order into Business Central
- Warehouse received printed pick list
- Shipping created manual labels
- Customer received email confirmation (manually sent)
- Time: 45 minutes per order
- Error rate: ~10% due to manual data entry
The Vision
Automated order flow:
- Customer clicks “Place Order” on website
- Order instantly created in Business Central
- Warehouse automatically receives pick list
- Inventory automatically reserved
- Shipping labels auto-generated
- Customer receives immediate email confirmation
- Time: 2 minutes
- Error rate: <1%
Building the Sales Order API
Step 1: Sales Order Header API
al
page 50102 "Mehta Sales Order API"
{
PageType = API;
APIPublisher = 'mehtagifts';
APIGroup = 'sales';
APIVersion = 'v1.0';
EntityName = 'salesOrder';
EntitySetName = 'salesOrders';
SourceTable = "Sales Header";
SourceTableView = where("Document Type" = const(Order));
DelayedInsert = true;
ODataKeyFields = SystemId;
layout
{
area(content)
{
repeater(Group)
{
field(id; Rec.SystemId)
{
Caption = 'ID';
Editable = false;
}
field(orderNo; Rec."No.")
{
Caption = 'Order Number';
Editable = false;
}
field(customerNo; Rec."Sell-to Customer No.")
{
Caption = 'Customer Number';
}
field(customerName; Rec."Sell-to Customer Name")
{
Caption = 'Customer Name';
}
field(orderDate; Rec."Order Date")
{
Caption = 'Order Date';
}
field(shipmentDate; Rec."Shipment Date")
{
Caption = 'Shipment Date';
}
field(status; Rec.Status)
{
Caption = 'Status';
Editable = false;
}
field(amount; Rec.Amount)
{
Caption = 'Amount';
Editable = false;
}
field(amountIncludingVAT; Rec."Amount Including VAT")
{
Caption = 'Total Amount';
Editable = false;
}
field(shipToName; Rec."Ship-to Name")
{
Caption = 'Ship To Name';
}
field(shipToAddress; Rec."Ship-to Address")
{
Caption = 'Ship To Address';
}
field(shipToCity; Rec."Ship-to City")
{
Caption = 'Ship To City';
}
field(shipToPostCode; Rec."Ship-to Post Code")
{
Caption = 'Ship To Postal Code';
}
field(lastModifiedDateTime; Rec.SystemModifiedAt)
{
Caption = 'Last Modified';
Editable = false;
}
// Part for order lines
part(salesOrderLines; "Mehta Sales Line API")
{
Caption = 'Sales Order Lines';
EntityName = 'salesOrderLine';
EntitySetName = 'salesOrderLines';
SubPageLink = "Document No." = field("No.");
}
}
}
}
}
Step 2: Sales Line API
al
page 50103 "Mehta Sales Line API"
{
PageType = API;
APIPublisher = 'mehtagifts';
APIGroup = 'sales';
APIVersion = 'v1.0';
EntityName = 'salesOrderLine';
EntitySetName = 'salesOrderLines';
SourceTable = "Sales Line";
SourceTableView = where("Document Type" = const(Order));
DelayedInsert = true;
ODataKeyFields = SystemId;
layout
{
area(content)
{
repeater(Group)
{
field(id; Rec.SystemId)
{
Caption = 'ID';
Editable = false;
}
field(documentNo; Rec."Document No.")
{
Caption = 'Document Number';
}
field(lineNo; Rec."Line No.")
{
Caption = 'Line Number';
}
field(itemNo; Rec."No.")
{
Caption = 'Item Number';
}
field(description; Rec.Description)
{
Caption = 'Description';
}
field(quantity; Rec.Quantity)
{
Caption = 'Quantity';
}
field(unitPrice; Rec."Unit Price")
{
Caption = 'Unit Price';
}
field(lineDiscount; Rec."Line Discount %")
{
Caption = 'Line Discount %';
}
field(lineAmount; Rec."Line Amount")
{
Caption = 'Line Amount';
Editable = false;
}
field(unitOfMeasure; Rec."Unit of Measure Code")
{
Caption = 'Unit of Measure';
}
}
}
}
}
Creating a Sales Order via API
The Corporate Order Scenario
Tech Solutions Pvt Ltd orders 100 gift hampers + 100 gift cards through the website.
Request:
http
POST /api/mehtagifts/sales/v1.0/companies(company-id)/salesOrders
Content-Type: application/json
Authorization: Bearer {access-token}
{
"customerNo": "CORP001",
"customerName": "Tech Solutions Pvt Ltd",
"orderDate": "2024-10-20",
"shipmentDate": "2024-10-25",
"shipToName": "Tech Solutions Pvt Ltd",
"shipToAddress": "Plot 45, Tech Park",
"shipToCity": "Mumbai",
"shipToPostCode": "400001",
"salesOrderLines": [
{
"itemNo": "GIFT-HAMPER-01",
"description": "Premium Corporate Gift Hamper",
"quantity": 100,
"unitPrice": 1500.00,
"lineDiscount": 10
},
{
"itemNo": "GIFT-CARD-500",
"description": "Mehta Gifts Gift Card - Rs. 500",
"quantity": 100,
"unitPrice": 500.00,
"lineDiscount": 0
}
]
}
Response:
json
{
"@odata.context": "https://api.businesscentral.dynamics.com/v2.0/production/api/mehtagifts/sales/v1.0/$metadata#companies(company-id)/salesOrders/$entity",
"id": "9d0e1f2a-3b4c-5d6e-7f8a-9b0c1d2e3f4a",
"orderNo": "SO-2024-001234",
"customerNo": "CORP001",
"customerName": "Tech Solutions Pvt Ltd",
"orderDate": "2024-10-20",
"shipmentDate": "2024-10-25",
"status": "Open",
"amount": 185000.00,
"amountIncludingVAT": 218300.00,
"shipToName": "Tech Solutions Pvt Ltd",
"shipToAddress": "Plot 45, Tech Park",
"shipToCity": "Mumbai",
"shipToPostCode": "400001",
"lastModifiedDateTime": "2024-10-20T10:30:00Z",
"salesOrderLines": [
{
"id": "0e1f2a3b-4c5d-6e7f-8a9b-0c1d2e3f4a5b",
"documentNo": "SO-2024-001234",
"lineNo": 10000,
"itemNo": "GIFT-HAMPER-01",
"description": "Premium Corporate Gift Hamper",
"quantity": 100,
"unitPrice": 1500.00,
"lineDiscount": 10,
"lineAmount": 135000.00,
"unitOfMeasure": "PCS"
},
{
"id": "1f2a3b4c-5d6e-7f8a-9b0c-1d2e3f4a5b6c",
"documentNo": "SO-2024-001234",
"lineNo": 20000,
"itemNo": "GIFT-CARD-500",
"description": "Mehta Gifts Gift Card - Rs. 500",
"quantity": 100,
"unitPrice": 500.00,
"lineDiscount": 0,
"lineAmount": 50000.00,
"unitOfMeasure": "PCS"
}
]
}
What Happens Automatically:
Once the order is created in Business Central:
- Inventory Reservation – 100 gift hampers are reserved
- Warehouse Notification – Warehouse staff receive pick list
- Packing Documentation – Packing slip is generated
- Customer Email – Order confirmation sent automatically
- Loyalty Points – Customer earns points for the purchase
- All of this happens in seconds, without any manual intervention.
Real-Time Inventory Synchronization
The Overselling Problem
- At 3:00 PM, the website showed “25 Diya Sets Available”
- At 3:15 PM, a customer ordered 10 online
- Simultaneously, at the physical shop, staff sold 12 units
- At 3:20 PM, another website customer tried to order 15 sets
Problem: Only 3 remained in stock
This overselling led to angry customers and order cancellations.
The Solution: Webhook-Based Updates
Instead of checking inventory every few minutes, the system would push updates instantly whenever inventory changed.
Creating the Inventory Update Codeunit
al
codeunit 50100 "Mehta Inventory Manager"
{
procedure UpdateInventoryAfterSale(ItemNo: Code[20])
var
Item: Record Item;
InventoryUpdateEvent: Record "Mehta Inventory Event";
begin
if Item.Get(ItemNo) then begin
Item.CalcFields(Inventory);
// Log the inventory change
InventoryUpdateEvent.Init();
InventoryUpdateEvent."Item No." := ItemNo;
InventoryUpdateEvent."New Inventory" := Item.Inventory;
InventoryUpdateEvent."Update DateTime" := CurrentDateTime;
InventoryUpdateEvent.Insert(true);
// Trigger webhook notification
SendInventoryWebhook(ItemNo, Item.Inventory);
end;
end;
local procedure SendInventoryWebhook(ItemNo: Code[20]; NewInventory: Decimal)
var
HttpClient: HttpClient;
HttpContent: HttpContent;
HttpResponse: HttpResponseMessage;
WebhookUrl: Text;
JsonPayload: Text;
begin
WebhookUrl := 'https://mehtagifts.com/webhooks/inventory-update';
JsonPayload := StrSubstNo('{"itemNo": "%1", "inventory": %2, "timestamp": "%3"}',
ItemNo, NewInventory, CurrentDateTime);
HttpContent.WriteFrom(JsonPayload);
HttpContent.GetHeaders().Clear();
HttpContent.GetHeaders().Add('Content-Type', 'application/json');
if HttpClient.Post(WebhookUrl, HttpContent, HttpResponse) then begin
if not HttpResponse.IsSuccessStatusCode then
Error('Webhook failed: %1', HttpResponse.ReasonPhrase);
end;
end;
[EventSubscriber(ObjectType::Table, Database::"Sales Line", 'OnAfterInsertEvent', '', false, false)]
local procedure OnSalesLineInsert(var Rec: Record "Sales Line")
begin
if Rec.Type = Rec.Type::Item then
UpdateInventoryAfterSale(Rec."No.");
end;
[EventSubscriber(ObjectType::Table, Database::"Sales Line", 'OnAfterModifyEvent', '', false, false)]
local procedure OnSalesLineModify(var Rec: Record "Sales Line")
begin
if Rec.Type = Rec.Type::Item then
UpdateInventoryAfterSale(Rec."No.");
end;
}
Website Integration (Node.js)
The website listens for webhook notifications:
javascript
const express = require('express');
const app = express();
const io = require('socket.io')(server);
app.post('/webhooks/inventory-update', express.json(), async (req, res) => {
const { itemNo, inventory, timestamp } = req.body;
try {
// Update database
await db.products.update(
{ itemNumber: itemNo },
{ $set: { inventory: inventory, lastUpdated: timestamp } }
);
// Broadcast to all connected clients
io.emit('inventory-update', {
itemNo: itemNo,
newInventory: inventory
});
// Check for low stock alert
if (inventory < 10) {
io.emit('low-stock-alert', {
itemNo: itemNo,
inventory: inventory
});
}
res.status(200).json({ success: true });
} catch (error) {
console.error('Inventory update failed:', error);
res.status(500).json({ success: false, error: error.message });
}
});
// Client-side real-time update
const socket = io();
socket.on('inventory-update', (data) => {
const productElement = document.getElementById(`product-${data.itemNo}`);
if (productElement) {
const inventorySpan = productElement.querySelector('.inventory-count');
inventorySpan.textContent = data.newInventory;
// Update UI based on stock level
if (data.newInventory === 0) {
productElement.classList.add('out-of-stock');
productElement.querySelector('.buy-button').disabled = true;
productElement.querySelector('.buy-button').textContent = 'Out of Stock';
} else if (data.newInventory < 5) {
productElement.classList.add('low-stock');
productElement.querySelector('.stock-warning').textContent =
`Only ${data.newInventory} left!`;
}
}
});
Now when a sale happens anywhere – online or in-store – all systems update instantly. No more overselling. No more disappointed customers.
The Transformation
After implementing these integrations, Mehta Gifts saw remarkable changes:
Customer Experience:
- Loyalty points unified across all channels
- Real-time inventory prevents disappointment
- Instant order confirmation builds trust
- Seamless redemption works everywhere
Operational Efficiency:
- Order processing time: 45 minutes → 2 minutes (96% reduction)
- Order accuracy: 90% → 99.5%
- Customer complaints: Reduced by 85%
- Staff productivity: Increased by 60%
Business Growth:
- Online revenue: Up 325% in 6 months
- Corporate orders: Increased 400%
- Customer satisfaction: 3.5/5 → 4.7/5 stars
- Repeat purchases: Up 45%
Mrs. Sharma became a Platinum member within 3 months. She could check her points on WhatsApp, earn points in-store, and redeem them online. The experience was seamless, and she told all her friends about Mehta Gifts.
What’s Next?
- In Part 4, the final part of our series, we’ll cover the critical aspects that make these integrations production-ready:
- OAuth 2.0 authentication and security
- Role-based access control
- Comprehensive monitoring and health checks
- Error handling strategies
- Best practices and lessons learned
- The systems are built. The integrations are working. Now we need to make sure they’re secure, reliable, and scalable.
Coming Up in Part 4: “Securing & Scaling: Production-Ready Business Central APIs” – where we’ll ensure these powerful integrations are bulletproof, secure, and ready for enterprise-scale operations.