Skip to main content
ElastiCORE 3.0 is a release candidate: 3.0.261022-rc1 on Maven Central. Projects on the 2.2 line: see the 2.2 documentation.
Version: 3.0

Step 3: orders

The model​

blueprint/petstore/model/entity.yml
  # ---- orders -----------------------------------------------------------------

PurchaseOrder:
# 주문은 로그인한 고객이 자기 것으로, 조회·취소는 본인, 배송·삭제는 관리자
meta: entity @root @expose(20) @event(Placed(total), Shipped, Cancelled) @owner(customerId)
@access(read=owner, create=user, cancel=owner) @terms(order, purchase, 구매) -- 주문
fields:
orderId: string(36) @id @genid
customer: Customer @ManyToOne # -> customerId
orderDate: date @behavior
status: OrderStatus
deliveryAddress: Address @Embedded
creditCard: CreditCard! @Embedded
totalWithoutVat: money(12.2) @default(0) @compute(sum(lines.subTotal))
vatRate: decimal(5.2) @default(0) @behavior
vat: money(12.2) @default(0) @behavior
discountRate: decimal(5.2) @default(0) @behavior
discount: money(12.2) @default(0) @behavior
total: money(12.2) @default(0) @behavior
lines: List<OrderLine> @OneToMany(mappedBy="order") @dtype(List<OrderLineDTO>) @minsize(1, 'the cart is empty')
version: long @jpa:Version
states: # 주문의 생명주기: 명령마다 어디서 어디로
status:
initial: PLACED
ship: PLACED -> SHIPPED
cancel: PLACED -> CANCELLED
delete: CANCELLED # 지울 수 있는 상태: 취소된 주문만

OrderLine:
meta: entity @behavior -- 주문 줄
fields:
lineId: string(36) @id @genid
order: PurchaseOrder @ManyToOne
item: Item @ManyToOne # -> itemId
quantity: quantity! @min(1)
unitCost: money(10.2) @snapshot(item.unitCost) # 주문 당시 가격을 복사해 둠
subTotal: money(12.2) @compute(unitCost * quantity)
blueprint/petstore/model/enum.yml
  OrderStatus:
meta: enum @db(code) @json(code) -- 주문 상태
fields:
code: string(3) @code
label: string(20)
enum:
PLACED: PLC, Placed
SHIPPED: SHP, Shipped
CANCELLED: CAN, Cancelled
blueprint/petstore/model/dto.yml (excerpt)
dto:
PurchaseOrderDTO:
meta: dto @template(PurchaseOrder)

OrderLineDTO:
meta: dto @template(OrderLine)
fields:
itemName: string @ref(item.name) # 응답에 품목 이름

(The Korean comments: an order is the signed-in customer's, reading and cancelling it are theirs, shipping and deleting the administrator's; the order's lifecycle, per command from where to where; deletable states - cancelled orders only; the price when ordered is copied; the item's name in the answer.)

What each part does​

PartEffect
@owner(customerId) @access(read=owner, create=user, cancel=owner)a signed-in customer orders (and becomes the owner whatever the request says); reads, searches and cancels only their own; ship and delete are the administrator's
states:POST /{orderId}/ship and /cancel; a step from the wrong state answers 409; delete only when CANCELLED
status: OrderStatusread-only in the DTO - a request cannot set it
@snapshot(item.unitCost)the line keeps the price when it was ordered; a later price change does not touch it
@compute(...)subtotals and totalWithoutVat worked out on every save; values sent are ignored
@behavior fieldsonly the root's methods set them - the shop's VAT and discount (below)
@minsize(1, 'the cart is empty')an order without lines - [] or missing - answers 400
@event(Placed(total), Shipped, Cancelled)ship and cancel raise Shipped and Cancelled; Placed is raised by the code below
version: long @jpa:Versiontwo saves of the same order at once: the later one answers 409
PurchaseOrder has no @editable fieldno PUT: an order changes by its commands only

The shop's own policy, by hand​

VAT and a discount are the business's policy, from settings - the one part of the order the DSL does not say. The root PurchaseOrder is split: PurchaseOrderBase is regenerated, PurchaseOrder is yours.

domain/model/PurchaseOrder.java (yours)
@Entity
@Table(name = "ps_purchase_order")
@DynamicUpdate
public class PurchaseOrder extends PurchaseOrderBase {

private static final BigDecimal HUNDRED = BigDecimal.valueOf(100);

public void place(LocalDate today, Function<Long, Address> homeOf, BigDecimal vatRate, BigDecimal discountRate) {
if (getOrderDate() != null) throw new InvariantViolationException("order " + getOrderId() + " is already placed");
if (getDeliveryAddress() == null) setDeliveryAddress(homeOf.apply(getCustomerId()));
if (getDeliveryAddress() == null) throw new InvariantViolationException("where should the order go? no delivery address");

Money withoutVat = getTotalWithoutVat();
Money vat = percent(withoutVat, vatRate);
Money discount = percent(withoutVat.plus(vat), discountRate);

setOrderDate(today);
setVatRate(vatRate);
setVat(vat);
setDiscountRate(discountRate);
setDiscount(discount);
setTotal(withoutVat.plus(vat).minus(discount).rounded(2));
raisePlaced();
}

private static Money percent(Money amount, BigDecimal rate) {
return amount.times(rate.divide(HUNDRED, 6, RoundingMode.HALF_UP)).rounded(2);
}
}
application/PurchaseOrderService.java (yours, abridged)
@Override
protected void beforeSave(PurchaseOrder order) {
if (order.getOrderDate() == null) {
order.place(LocalDate.now(clock), this::homeOf, vatRate, discountRate);
}
}

The rates come from application.yml:

application.yml
petstore:
vat-rate: 5.5 # percent, added to the lines
discount-rate: 2.25 # percent, taken off the VAT-inclusive amount

What it answers​

Real requests to the sample application, and its answers:

order (not signed in)                      → 401 "Sign in to create PurchaseOrder"
order (sends unit cost 0.01, status SHP) → 200 unit cost 10.00, status PLC - the server decides
ship (as a customer) → 403 "ship PurchaseOrder is for the administrator"
cancel (a shipped order) → 409 "PurchaseOrder cannot cancel when status is SHIPPED; it must be [PLACED]"
cancel (shipped, Accept-Language: ko) → 409 the same in Korean, code: state.PurchaseOrder.status.cancel
delete (a shipped order) → 409 "PurchaseOrder cannot be deleted when status is SHIPPED; it must be [CANCELLED]"
someone else's order → 404

A body for placing an order (signed in):

{ "lines": [ { "itemId": 1000, "quantity": 2 } ],
"creditCard": { "creditCardNumber": "4111111111111111", "creditCardType": "VISA", "creditCardExpDate": "12/30" } }

The answer carries each line's unitCost and subTotal, the totals, the delivery address (the home address when none was sent) and no card number. Validation comes before the access check: the same request without the card answers 400 creditCard: must not be null even for a visitor.

Next: the back office, events, metrics and messages.