Step 4: the back office, events, metrics and messages
The back office
Signed in as the administrator, the shop's header shows Back office: the orders, newest first, with a Ship button where shipping is possible. The screen is plain TypeScript on the generated client - the server decides who may ship and from which state:
async function admin(): Promise<void> {
if (customer?.role !== UserRole.ADMIN) return go('#/signin');
const page = await Orders.search({ sortColumn: 'orderDate', sortAscending: false, pageSize: 50 });
render(`<h1>Back office - orders</h1>${orderTable(page.content, true)}<p class="error" id="error"></p>`);
on('button.ship', 'click', async (el) => {
try {
await Orders.ship(el.dataset.id!);
flash('Shipped');
admin();
} catch (e) {
document.getElementById('error')!.textContent = problem(e);
}
});
}
The button is shown when Orders.canShip(o) - the generated twin of the lifecycle's canShip(). The
same search answers a customer with their own orders only (@access(read=owner)), and the
administrator with all of them. A customer's order page shows Cancel this order when
Orders.canCancel(o).
curl -s -c jar -b jar -X POST https://petstore.elasticore.io/api/petstore/customer/signin \
-H 'Content-Type: application/json' -d '{"login":"xsko","password":"xsko"}'
curl -s -c jar -b jar -X POST https://petstore.elasticore.io/api/petstore/purchaseOrder/{orderId}/ship
An administrator's summary - order counts per status and revenue - is an operation of your own: see
the shop recipe's report() behind ACCESS.require(currentUser, "report") in
Your logic.
Events
@event(Placed(total), Shipped, Cancelled) gives PurchaseOrderPlacedEvent (carrying total),
PurchaseOrderShippedEvent and PurchaseOrderCancelledEvent. ship and cancel raise theirs as
lifecycle steps; place() raises Placed. They are published when the order is saved; an
@EventListener of yours receives them - for example a log line per shipped order:
@EventListener
void shipped(PurchaseOrderShippedEvent e) {
log.info("order {} SHIPPED", e.getOrderId());
}
Shipped carries no fields here, so the event has the order's id and occurredOn. To log the total
too, declare it as Shipped(total), as the agent kit's shop recipe does.
With outbox: true the same events would go to a broker through a transactional outbox.
Metrics for BI and AI
# The shop's measures - what the business means by revenue, an order, the average order.
# Exported to the Apache Ossie semantic model (blueprint/semantic_model.yml) as metrics,
# so a BI or AI tool asks for "revenue" and gets this definition, not its own guess.
metrics:
revenue: sum(PurchaseOrder.total) where status != CANCELLED @terms(sales, 매출액) -- 매출 (취소 제외, VAT 포함·할인 후 금액)
orderCount: count(PurchaseOrder) where status != CANCELLED @terms(orders, 주문 건수) -- 주문 수 (취소 제외)
averageOrder: revenue / orderCount @terms(AOV, 객단가) -- 주문 1건 평균 금액
shippedOrders: count(PurchaseOrder) where status = SHIPPED -- 배송된 주문 수
cancelledOrders: count(PurchaseOrder) where status = CANCELLED -- 취소된 주문 수
customerCount: count(Customer) where role = USER @terms(members, 회원 수) -- 고객 수 (관리자 제외)
With rules = ['ossie-export'] the build writes blueprint/semantic_model.yml: every table, column,
join, enum code and these measures as SQL. MetricsTest places, ships and cancels orders, runs each
exported metric on the database and compares it with the same numbers worked out from the entities;
SemanticModelTest checks every exported column against the database. See
Apache Ossie.
Korean error messages
The build writes every code the domain can answer to
src/main/resources/i18n/petstore-messages.properties; the sample translates the ones it needs in
petstore-messages_ko.properties beside it:
error.state.title=지금 상태에서는 할 수 없는 요청
state.PurchaseOrder.status.cancel=주문 상태가 {0}이면 취소할 수 없습니다 (가능한 상태: {1})
invariant.PurchaseOrder.lines.minsize=장바구니가 비어 있습니다
Size.lines=장바구니가 비어 있습니다
typeMismatch.lines.quantity=수량은 0 이상의 정수입니다
POST /api/petstore/purchaseOrder/{id}/cancel
Accept-Language: ko
HTTP/1.1 409
{ "title": "지금 상태에서는 할 수 없는 요청",
"detail": "주문 상태가 SHIPPED이면 취소할 수 없습니다 (가능한 상태: [PLACED])",
"code": "state.PurchaseOrder.status.cancel", "status": 409 }
Without Accept-Language, or for a language without a translation, the answer is English - never the
server machine's language. See Messages and languages.
The screen
web/src (main.ts, shop.ts, cart.ts) is bundled into static/app.js by the webBuild task and
type-checked against src/main/ts. It uses the generated OrderStatusLabel, validateAddress,
ApiError.fieldErrors, canShip / canCancel, and CustomerApi.signIn / signOut / me; signing
up is the one hand-written endpoint:
export { signIn, signOut, me } from '../../src/main/ts/CustomerApi';
export const signUp = (customer: CustomerDTO) =>
request<CustomerDTO>('POST', '/api/petstore/account/signup', customer);
Next: evolving a generated project - what happens when the business asks for more.