Skip to main content

Customer Portal

This scenario demonstrates building a customer-facing portal where customers can view orders, check status, and submit requests.


Business Requirements

┌─────────────────────────────────────────────────────────────────────────────┐
│ CUSTOMER PORTAL REQUIREMENTS │
└─────────────────────────────────────────────────────────────────────────────┘

✓ Customers can view their order history
✓ Check order status and tracking
✓ View and download invoices
✓ Submit support requests
✓ Secure access via RESTlet API
✓ Mobile-friendly interface

Architecture

┌─────────────────────────────────────────────────────────────────────────────┐
│ PORTAL ARCHITECTURE │
└─────────────────────────────────────────────────────────────────────────────┘

┌──────────────────┐
│ External App │
│ (Website/Mobile)│
└────────┬─────────┘

│ HTTPS + OAuth


┌──────────────────────────────────────────────────────────────────┐
│ RESTLET API │
│ ────────────────────────────────────────────────────────────────│
│ GET /orders - List customer orders │
│ GET /orders/:id - Get order details │
│ GET /invoices - List customer invoices │
│ POST /support - Submit support ticket │
└──────────────────────────────────────────────────────────────────┘


┌──────────────────────────────────────────────────────────────────┐
│ NETSUITE DATA │
│ ────────────────────────────────────────────────────────────────│
│ Sales Orders, Invoices, Items, Cases │
└──────────────────────────────────────────────────────────────────┘

Customer Portal RESTlet

src/FileCabinet/SuiteScripts/RESTlets/customer_portal_rl.js

/**
* @NApiVersion 2.1
* @NScriptType Restlet
* @NModuleScope SameAccount
*/
define(['N/search', 'N/record', 'N/log', 'N/error', 'N/format'],
(search, record, log, error, format) => {

/**
* GET - Retrieve orders or order details
*/
const get = (context) => {
log.debug('Portal GET', JSON.stringify(context));

try {
const customerId = context.customerId;
const action = context.action || 'orders';
const orderId = context.orderId;

if (!customerId) {
return createError('MISSING_CUSTOMER', 'Customer ID is required');
}

switch (action) {
case 'orders':
return getOrders(customerId, context);
case 'order':
return getOrderDetails(customerId, orderId);
case 'invoices':
return getInvoices(customerId);
case 'invoice':
return getInvoiceDetails(customerId, context.invoiceId);
default:
return createError('INVALID_ACTION', 'Unknown action');
}

} catch (e) {
log.error('GET Error', e.message);
return createError(e.name, e.message);
}
};

/**
* POST - Create support ticket
*/
const post = (context) => {
log.debug('Portal POST', JSON.stringify(context));

try {
const { customerId, subject, message, priority } = context;

if (!customerId || !subject || !message) {
return createError('MISSING_FIELDS', 'customerId, subject, and message are required');
}

// Create support case
const supportCase = record.create({
type: record.Type.SUPPORT_CASE,
isDynamic: true
});

supportCase.setValue({ fieldId: 'company', value: customerId });
supportCase.setValue({ fieldId: 'title', value: subject });
supportCase.setValue({ fieldId: 'incomingmessage', value: message });
supportCase.setValue({ fieldId: 'priority', value: priority || 2 }); // Medium

const caseId = supportCase.save();

return {
success: true,
caseId: caseId,
message: 'Support ticket created successfully'
};

} catch (e) {
log.error('POST Error', e.message);
return createError(e.name, e.message);
}
};

/**
* Get customer orders
*/
const getOrders = (customerId, params) => {
const limit = parseInt(params.limit) || 50;
const offset = parseInt(params.offset) || 0;
const status = params.status;

const filters = [
['entity', 'anyof', customerId],
'AND',
['mainline', 'is', 'T']
];

if (status) {
filters.push('AND', ['status', 'anyof', status]);
}

const orderSearch = search.create({
type: search.Type.SALES_ORDER,
filters: filters,
columns: [
search.createColumn({ name: 'tranid' }),
search.createColumn({ name: 'trandate', sort: search.Sort.DESC }),
search.createColumn({ name: 'status' }),
search.createColumn({ name: 'total' }),
search.createColumn({ name: 'shipdate' }),
search.createColumn({ name: 'custbody_tracking_number' })
]
});

const orders = [];
const results = orderSearch.run().getRange({ start: offset, end: offset + limit });

results.forEach(result => {
orders.push({
id: result.id,
orderNumber: result.getValue('tranid'),
date: result.getValue('trandate'),
status: result.getText('status'),
total: parseFloat(result.getValue('total')),
shipDate: result.getValue('shipdate'),
trackingNumber: result.getValue('custbody_tracking_number')
});
});

return {
success: true,
count: orders.length,
offset: offset,
orders: orders
};
};

/**
* Get order details with line items
*/
const getOrderDetails = (customerId, orderId) => {
if (!orderId) {
return createError('MISSING_ORDER', 'Order ID is required');
}

// Verify customer owns this order
const orderRecord = record.load({
type: record.Type.SALES_ORDER,
id: orderId
});

if (orderRecord.getValue({ fieldId: 'entity' }) != customerId) {
return createError('ACCESS_DENIED', 'Order does not belong to customer');
}

// Get order header
const order = {
id: orderId,
orderNumber: orderRecord.getValue({ fieldId: 'tranid' }),
date: orderRecord.getValue({ fieldId: 'trandate' }),
status: orderRecord.getText({ fieldId: 'status' }),
total: orderRecord.getValue({ fieldId: 'total' }),
subtotal: orderRecord.getValue({ fieldId: 'subtotal' }),
shippingCost: orderRecord.getValue({ fieldId: 'shippingcost' }),
shipDate: orderRecord.getValue({ fieldId: 'shipdate' }),
trackingNumber: orderRecord.getValue({ fieldId: 'custbody_tracking_number' }),
memo: orderRecord.getValue({ fieldId: 'memo' }),
items: []
};

// Get line items
const lineCount = orderRecord.getLineCount({ sublistId: 'item' });

for (let i = 0; i < lineCount; i++) {
order.items.push({
item: orderRecord.getSublistText({ sublistId: 'item', fieldId: 'item', line: i }),
description: orderRecord.getSublistValue({ sublistId: 'item', fieldId: 'description', line: i }),
quantity: orderRecord.getSublistValue({ sublistId: 'item', fieldId: 'quantity', line: i }),
rate: orderRecord.getSublistValue({ sublistId: 'item', fieldId: 'rate', line: i }),
amount: orderRecord.getSublistValue({ sublistId: 'item', fieldId: 'amount', line: i })
});
}

// Get shipping address
const shipAddr = orderRecord.getSubrecord({ fieldId: 'shippingaddress' });
order.shippingAddress = {
addressee: shipAddr.getValue({ fieldId: 'addressee' }),
addr1: shipAddr.getValue({ fieldId: 'addr1' }),
addr2: shipAddr.getValue({ fieldId: 'addr2' }),
city: shipAddr.getValue({ fieldId: 'city' }),
state: shipAddr.getValue({ fieldId: 'state' }),
zip: shipAddr.getValue({ fieldId: 'zip' }),
country: shipAddr.getValue({ fieldId: 'country' })
};

return {
success: true,
order: order
};
};

/**
* Get customer invoices
*/
const getInvoices = (customerId) => {
const invoiceSearch = search.create({
type: search.Type.INVOICE,
filters: [
['entity', 'anyof', customerId],
'AND',
['mainline', 'is', 'T']
],
columns: [
'tranid',
search.createColumn({ name: 'trandate', sort: search.Sort.DESC }),
'status',
'total',
'amountremaining',
'duedate'
]
});

const invoices = [];

invoiceSearch.run().each(result => {
invoices.push({
id: result.id,
invoiceNumber: result.getValue('tranid'),
date: result.getValue('trandate'),
status: result.getText('status'),
total: parseFloat(result.getValue('total')),
amountDue: parseFloat(result.getValue('amountremaining')),
dueDate: result.getValue('duedate')
});
return true;
});

return {
success: true,
invoices: invoices
};
};

/**
* Get invoice details
*/
const getInvoiceDetails = (customerId, invoiceId) => {
if (!invoiceId) {
return createError('MISSING_INVOICE', 'Invoice ID is required');
}

const invoiceRecord = record.load({
type: record.Type.INVOICE,
id: invoiceId
});

if (invoiceRecord.getValue({ fieldId: 'entity' }) != customerId) {
return createError('ACCESS_DENIED', 'Invoice does not belong to customer');
}

const invoice = {
id: invoiceId,
invoiceNumber: invoiceRecord.getValue({ fieldId: 'tranid' }),
date: invoiceRecord.getValue({ fieldId: 'trandate' }),
dueDate: invoiceRecord.getValue({ fieldId: 'duedate' }),
status: invoiceRecord.getText({ fieldId: 'status' }),
total: invoiceRecord.getValue({ fieldId: 'total' }),
amountPaid: invoiceRecord.getValue({ fieldId: 'amountpaid' }),
amountDue: invoiceRecord.getValue({ fieldId: 'amountremaining' }),
items: []
};

const lineCount = invoiceRecord.getLineCount({ sublistId: 'item' });

for (let i = 0; i < lineCount; i++) {
invoice.items.push({
item: invoiceRecord.getSublistText({ sublistId: 'item', fieldId: 'item', line: i }),
quantity: invoiceRecord.getSublistValue({ sublistId: 'item', fieldId: 'quantity', line: i }),
rate: invoiceRecord.getSublistValue({ sublistId: 'item', fieldId: 'rate', line: i }),
amount: invoiceRecord.getSublistValue({ sublistId: 'item', fieldId: 'amount', line: i })
});
}

return {
success: true,
invoice: invoice
};
};

/**
* Create error response
*/
const createError = (code, message) => {
return {
success: false,
error: {
code: code,
message: message
}
};
};

return { get, post };
});

API Usage Examples

Get Orders

GET /restlet.nl?script=customscript_customer_portal&deploy=1
&customerId=12345
&action=orders
&limit=10

Get Order Details

GET /restlet.nl?script=customscript_customer_portal&deploy=1
&customerId=12345
&action=order
&orderId=67890

Submit Support Ticket

POST /restlet.nl?script=customscript_customer_portal&deploy=1

{
"customerId": "12345",
"subject": "Order Inquiry",
"message": "I need help with my order",
"priority": 2
}

Security Considerations

┌─────────────────────────────────────────────────────────────────────────────┐
│ SECURITY CHECKLIST │
└─────────────────────────────────────────────────────────────────────────────┘

☑ Always verify customer owns requested data
☑ Use Token-Based Authentication
☑ Validate all input parameters
☑ Log all API access
☑ Rate limit requests
☑ Don't expose internal IDs in errors
☑ Use HTTPS only

Next Steps