diff --git a/commerce-api/README.adoc b/commerce-api/README.adoc
new file mode 100644
index 000000000..1fef6b4d0
--- /dev/null
+++ b/commerce-api/README.adoc
@@ -0,0 +1,3 @@
+
+The files in this directory are used as templates for the ant create-component target, +
+please do not modify them without understanding the effect that it will have on that build target.
\ No newline at end of file
diff --git a/commerce-api/api/commerce-api.rest.xml b/commerce-api/api/commerce-api.rest.xml
new file mode 100644
index 000000000..428d91858
--- /dev/null
+++ b/commerce-api/api/commerce-api.rest.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/commerce-api/build.gradle b/commerce-api/build.gradle
new file mode 100644
index 000000000..0056e3fb9
--- /dev/null
+++ b/commerce-api/build.gradle
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+dependencies {
+ //Examples of compile-time and runtime dependencies
+
+ //pluginLibsCompile 'junit:junit-dep:4.10'
+ //pluginLibsRuntime 'junit:junit-dep:4.10'
+}
+
+task install {
+ doLast {
+ // Install logic for this plugin
+ }
+}
+
+task uninstall {
+ doLast {
+ // uninstall logic for this plugin
+ }
+}
diff --git a/commerce-api/data/Commerce-apiSecurityGroupDemoData.xml b/commerce-api/data/Commerce-apiSecurityGroupDemoData.xml
new file mode 100644
index 000000000..6d0854339
--- /dev/null
+++ b/commerce-api/data/Commerce-apiSecurityGroupDemoData.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/commerce-api/data/Commerce-apiSecurityPermissionSeedData.xml b/commerce-api/data/Commerce-apiSecurityPermissionSeedData.xml
new file mode 100644
index 000000000..3ae2fe816
--- /dev/null
+++ b/commerce-api/data/Commerce-apiSecurityPermissionSeedData.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/commerce-api/ofbiz-component.xml b/commerce-api/ofbiz-component.xml
new file mode 100644
index 000000000..66b36b2b3
--- /dev/null
+++ b/commerce-api/ofbiz-component.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/commerce-api/servicedef/services.xml b/commerce-api/servicedef/services.xml
new file mode 100644
index 000000000..f899633d7
--- /dev/null
+++ b/commerce-api/servicedef/services.xml
@@ -0,0 +1,100 @@
+
+
+
+
+ Commerce-api Services
+
+ 1.0
+
+
+ Create Order for REST API
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Get Order details for REST API
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Update Order details for REST API
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Get Order Status for REST API
+
+
+
+
+
+
\ No newline at end of file
diff --git a/commerce-api/src/main/groovy/org/apache/ofbiz/commerce/OrderServices.groovy b/commerce-api/src/main/groovy/org/apache/ofbiz/commerce/OrderServices.groovy
new file mode 100644
index 000000000..d0766f372
--- /dev/null
+++ b/commerce-api/src/main/groovy/org/apache/ofbiz/commerce/OrderServices.groovy
@@ -0,0 +1,848 @@
+package org.apache.ofbiz.commerce
+
+import java.time.ZoneId
+import java.time.format.DateTimeFormatter
+import org.apache.ofbiz.base.util.Debug
+import org.apache.ofbiz.base.util.ObjectType
+import org.apache.ofbiz.base.util.UtilDateTime
+import org.apache.ofbiz.base.util.UtilValidate
+import org.apache.ofbiz.entity.GenericValue
+import org.apache.ofbiz.order.order.OrderReadHelper
+import org.apache.ofbiz.service.ServiceUtil
+
+Map commerceCreateOrder() {
+ Map result = ServiceUtil.returnSuccess()
+
+ // 1. Validation and input extraction
+ Map customerMap = parameters.customer
+ if (UtilValidate.isEmpty(customerMap)) {
+ return ServiceUtil.returnError('customer details are required.')
+ }
+ String customerExternalId = customerMap.customerExternalId
+ if (UtilValidate.isEmpty(customerExternalId)) {
+ return ServiceUtil.returnError('customerExternalId is required.')
+ }
+
+ List reqItems = parameters.items
+ if (UtilValidate.isEmpty(reqItems)) {
+ return ServiceUtil.returnError('items list is required and cannot be empty.')
+ }
+
+ try {
+ // Find or create customer party
+ String partyId = null
+ GenericValue party = from('Party').where('externalId', customerExternalId).queryFirst()
+ if (party) {
+ partyId = party.partyId
+ } else {
+ // Create a new Person party
+ Map createPersonCtx = [
+ userLogin: userLogin,
+ firstName: customerMap.firstName ?: 'First',
+ lastName: customerMap.lastName ?: 'Last',
+ externalId: customerExternalId
+ ]
+ Map createPersonResult = runService('createPerson', createPersonCtx)
+ if (ServiceUtil.isError(createPersonResult)) {
+ return ServiceUtil.returnError('Failed to create customer party: ' + ServiceUtil.getErrorMessage(createPersonResult))
+ }
+ partyId = createPersonResult.partyId
+ }
+
+ // Email setup
+ if (customerMap.email) {
+ Map emailRes = runService('getPartyEmail', [partyId: partyId, contactMechPurposeTypeId: 'PRIMARY_EMAIL'])
+ if (ServiceUtil.isError(emailRes) || !emailRes.emailAddress) {
+ runService('createPartyEmailAddress', [
+ userLogin: userLogin,
+ partyId: partyId,
+ contactMechPurposeTypeId: 'PRIMARY_EMAIL',
+ emailAddress: customerMap.email
+ ])
+ }
+ }
+
+ // Phone setup
+ if (customerMap.phone) {
+ String phoneStr = customerMap.phone
+ String countryCode = ''
+ String areaCode = ''
+ String contactNumber = phoneStr
+ if (phoneStr.startsWith('+')) {
+ String[] parts = phoneStr.substring(1).split('-')
+ if (parts.length >= 3) {
+ countryCode = parts[0]
+ areaCode = parts[1]
+ contactNumber = parts[2..-1].join('-')
+ } else if (parts.length == 2) {
+ areaCode = parts[0]
+ contactNumber = parts[1]
+ }
+ } else {
+ String[] parts = phoneStr.split('-')
+ if (parts.length >= 3) {
+ countryCode = parts[0]
+ areaCode = parts[1]
+ contactNumber = parts[2..-1].join('-')
+ } else if (parts.length == 2) {
+ areaCode = parts[0]
+ contactNumber = parts[1]
+ }
+ }
+ Map phoneRes = runService('getPartyTelephone', [partyId: partyId, contactMechPurposeTypeId: 'PRIMARY_PHONE'])
+ if (ServiceUtil.isError(phoneRes) || !phoneRes.contactNumber) {
+ runService('createPartyTelecomNumber', [
+ userLogin: userLogin,
+ partyId: partyId,
+ contactMechPurposeTypeId: 'PRIMARY_PHONE',
+ countryCode: countryCode,
+ areaCode: areaCode,
+ contactNumber: contactNumber
+ ])
+ }
+ }
+
+ // Helper closures with explicit type declarations instead of def
+ Closure cleanStr = { Object obj ->
+ if (obj) {
+ return obj.toString().trim()
+ }
+ return ''
+ }
+
+ Closure isAddressEqual = { GenericValue addr, Map addrMap ->
+ return cleanStr(addr.toName) == cleanStr(addrMap.toName) &&
+ cleanStr(addr.address1) == cleanStr(addrMap.address1) &&
+ cleanStr(addr.city) == cleanStr(addrMap.city) &&
+ cleanStr(addr.stateProvinceGeoId) == cleanStr(addrMap.stateProvinceGeoId) &&
+ cleanStr(addr.countryGeoId) == cleanStr(addrMap.countryGeoId) &&
+ cleanStr(addr.postalCode) == cleanStr(addrMap.postalCode)
+ }
+
+ Closure getOrCreatePostalAddress = { Map addrMap, String purposeTypeId ->
+ if (!addrMap) {
+ return null
+ }
+ List addresses = from('PartyAndPostalAddress')
+ .where('partyId', partyId)
+ .filterByDate()
+ .queryList()
+ for (Object addrObj : addresses) {
+ GenericValue addr = (GenericValue) addrObj
+ if (isAddressEqual(addr, addrMap)) {
+ return addr.contactMechId
+ }
+ }
+ Map createAddrCtx = [
+ userLogin: userLogin,
+ partyId: partyId,
+ contactMechPurposeTypeId: purposeTypeId,
+ toName: addrMap.toName,
+ address1: addrMap.address1,
+ city: addrMap.city,
+ stateProvinceGeoId: addrMap.stateProvinceGeoId,
+ countryGeoId: addrMap.countryGeoId,
+ postalCode: addrMap.postalCode
+ ]
+ Map createAddrResult = runService('createPartyPostalAddress', createAddrCtx)
+ if (ServiceUtil.isError(createAddrResult)) {
+ throw new IllegalArgumentException('Error creating postal address: ' + ServiceUtil.getErrorMessage(createAddrResult))
+ }
+ return (String) createAddrResult.contactMechId
+ }
+
+ String billingContactMechId = getOrCreatePostalAddress(parameters.billingAddress, 'BILLING_LOCATION')
+ String shippingContactMechId = getOrCreatePostalAddress(parameters.shippingAddress, 'SHIPPING_LOCATION')
+
+ // 2. Persist Order Header
+ String orderId = delegator.getNextSeqId('OrderHeader')
+
+ java.sql.Timestamp orderDate = parseDateTime(parameters.orderDate) ?: UtilDateTime.nowTimestamp()
+ java.sql.Timestamp entryDate = parseDateTime(parameters.entryDate) ?: UtilDateTime.nowTimestamp()
+
+ GenericValue orderHeader = delegator.makeValue('OrderHeader', [
+ orderId: orderId,
+ orderTypeId: parameters.orderTypeId ?: 'SALES_ORDER',
+ orderName: parameters.orderName,
+ externalId: parameters.externalId,
+ salesChannelEnumId: parameters.salesChannelEnumId ?: 'WEB_SALES_CHANNEL',
+ orderDate: orderDate,
+ entryDate: entryDate,
+ priority: parameters.priority,
+ currencyUom: parameters.currencyCode ?: 'USD',
+ statusId: parameters.status ?: 'ORDER_CREATED'
+ ])
+ if (parameters.agreements && parameters.agreements.size() > 0) {
+ orderHeader.agreementId = parameters.agreements[0].agreementId
+ }
+ delegator.create(orderHeader)
+
+ // Ensure Party roles exist & associate them with the order
+ Closure ensurePartyRole = { String pId, String rTypeId ->
+ GenericValue pr = from('PartyRole').where('partyId', pId, 'roleTypeId', rTypeId).queryOne()
+ if (!pr) {
+ delegator.create('PartyRole', [partyId: pId, roleTypeId: rTypeId])
+ }
+ }
+ ensurePartyRole(partyId, 'PLACING_CUSTOMER')
+ ensurePartyRole(partyId, 'BILL_TO_CUSTOMER')
+ ensurePartyRole(partyId, 'SHIP_TO_CUSTOMER')
+
+ delegator.create('OrderRole', [orderId: orderId, partyId: partyId, roleTypeId: 'PLACING_CUSTOMER'])
+ delegator.create('OrderRole', [orderId: orderId, partyId: partyId, roleTypeId: 'BILL_TO_CUSTOMER'])
+ delegator.create('OrderRole', [orderId: orderId, partyId: partyId, roleTypeId: 'SHIP_TO_CUSTOMER'])
+
+ // Order Contact Mechs (wrapped for line length rule)
+ if (shippingContactMechId) {
+ delegator.create('OrderContactMech', [
+ orderId: orderId,
+ contactMechId: shippingContactMechId,
+ contactMechPurposeTypeId: 'SHIPPING_LOCATION'
+ ])
+ }
+ if (billingContactMechId) {
+ delegator.create('OrderContactMech', [
+ orderId: orderId,
+ contactMechId: billingContactMechId,
+ contactMechPurposeTypeId: 'BILLING_LOCATION'
+ ])
+ }
+
+ // Create Default Ship Group
+ String shipGroupSeqId = '00001'
+ Map shipGroupData = [
+ orderId: orderId,
+ shipGroupSeqId: shipGroupSeqId,
+ carrierPartyId: '_NA_'
+ ]
+ Map firstItem = reqItems[0]
+ if (firstItem.shipmentMethodTypeId) {
+ shipGroupData.shipmentMethodTypeId = firstItem.shipmentMethodTypeId
+ }
+ if (firstItem.shippingInstructions) {
+ shipGroupData.shippingInstructions = firstItem.shippingInstructions
+ }
+ if (firstItem.facilityExternalId) {
+ shipGroupData.facilityId = firstItem.facilityExternalId
+ }
+ if (shippingContactMechId) {
+ shipGroupData.contactMechId = shippingContactMechId
+ }
+ delegator.create('OrderItemShipGroup', shipGroupData)
+
+ // 3. Persist Items and Adjustments
+ int itemSeq = 1
+ for (Object reqItemObj : reqItems) {
+ Map reqItem = (Map) reqItemObj
+ GenericValue product = from('Product').where('productId', reqItem.sku).queryOne()
+ if (!product) {
+ return ServiceUtil.returnError('Product not found with SKU/productId: ' + reqItem.sku)
+ }
+ String orderItemSeqId = String.format('%05d', itemSeq)
+
+ java.sql.Timestamp shipBeforeDate = parseDateTime(reqItem.shipByDate)
+ java.sql.Timestamp shipAfterDate = parseDateTime(reqItem.shipAfterDate)
+ java.sql.Timestamp estShipDate = parseDateTime(reqItem.estimatedShipDate)
+ java.sql.Timestamp estDeliveryDate = parseDateTime(reqItem.estimatedDeliveryDate)
+
+ GenericValue orderItem = delegator.makeValue('OrderItem', [
+ orderId: orderId,
+ orderItemSeqId: orderItemSeqId,
+ orderItemTypeId: 'PRODUCT_ORDER_ITEM',
+ externalId: reqItem.itemExternalId,
+ productId: product.productId,
+ quantity: reqItem.quantity ? new BigDecimal(reqItem.quantity) : BigDecimal.ONE,
+ unitPrice: reqItem.unitAmount ? new BigDecimal(reqItem.unitAmount) : BigDecimal.ZERO,
+ statusId: reqItem.status ?: 'ITEM_CREATED',
+ shipBeforeDate: shipBeforeDate,
+ shipAfterDate: shipAfterDate,
+ estimatedShipDate: estShipDate,
+ estimatedDeliveryDate: estDeliveryDate
+ ])
+ delegator.create(orderItem)
+
+ // Associate Item to Ship Group
+ delegator.create('OrderItemShipGroupAssoc', [
+ orderId: orderId,
+ orderItemSeqId: orderItemSeqId,
+ shipGroupSeqId: shipGroupSeqId,
+ quantity: reqItem.quantity ? new BigDecimal(reqItem.quantity) : BigDecimal.ONE
+ ])
+
+ // Item level adjustments (using each to avoid NestedForLoop)
+ if (reqItem.adjustments) {
+ reqItem.adjustments.each { Object adjObj ->
+ Map adj = (Map) adjObj
+ String adjId = delegator.getNextSeqId('OrderAdjustment')
+ GenericValue orderAdj = delegator.makeValue('OrderAdjustment', [
+ orderAdjustmentId: adjId,
+ orderAdjustmentTypeId: adj.type,
+ orderId: orderId,
+ orderItemSeqId: orderItemSeqId,
+ shipGroupSeqId: '_NA_',
+ amount: adj.amount ? new BigDecimal(adj.amount) : null,
+ productPromoId: adj.productPromoId
+ ])
+ delegator.create(orderAdj)
+ }
+ }
+
+ itemSeq++
+ }
+
+ // 4. Persist Order Level Adjustments
+ if (parameters.adjustments) {
+ for (Object adjObj : parameters.adjustments) {
+ Map adj = (Map) adjObj
+ String adjId = delegator.getNextSeqId('OrderAdjustment')
+ GenericValue orderAdj = delegator.makeValue('OrderAdjustment', [
+ orderAdjustmentId: adjId,
+ orderAdjustmentTypeId: adj.type,
+ orderId: orderId,
+ orderItemSeqId: '_NA_',
+ shipGroupSeqId: '_NA_',
+ amount: adj.amount ? new BigDecimal(adj.amount) : null,
+ productPromoId: adj.productPromoId
+ ])
+ delegator.create(orderAdj)
+ }
+ }
+
+ // 5. Persist Payment Preferences & Gateway Response
+ if (parameters.paymentPreferences) {
+ for (Object prefObj : parameters.paymentPreferences) {
+ Map pref = (Map) prefObj
+ String prefId = delegator.getNextSeqId('OrderPaymentPreference')
+ GenericValue orderPaymentPreference = delegator.makeValue('OrderPaymentPreference', [
+ orderPaymentPreferenceId: prefId,
+ orderId: orderId,
+ paymentMethodTypeId: pref.paymentMethodTypeId,
+ statusId: pref.statusId ?: 'PAYMENT_NOT_RECEIVED',
+ maxAmount: pref.maxAmount ? new BigDecimal(pref.maxAmount) : null
+ ])
+ delegator.create(orderPaymentPreference)
+
+ if (pref.transactionId) {
+ String respId = delegator.getNextSeqId('PaymentGatewayResponse')
+ GenericValue paymentGatewayResponse = delegator.makeValue('PaymentGatewayResponse', [
+ paymentGatewayResponseId: respId,
+ orderPaymentPreferenceId: prefId,
+ paymentMethodTypeId: pref.paymentMethodTypeId,
+ amount: pref.maxAmount ? new BigDecimal(pref.maxAmount) : null,
+ currencyUomId: pref.currencyUomId ?: parameters.currencyCode ?: 'USD',
+ referenceNum: pref.transactionId,
+ transactionDate: UtilDateTime.nowTimestamp()
+ ])
+ delegator.create(paymentGatewayResponse)
+ }
+ }
+ }
+
+ // 6. Persist Attributes
+ if (parameters.attributes) {
+ for (Object attrObj : parameters.attributes) {
+ Map attr = (Map) attrObj
+ GenericValue orderAttr = delegator.makeValue('OrderAttribute', [
+ orderId: orderId,
+ attrName: attr.name,
+ attrValue: attr.value
+ ])
+ delegator.create(orderAttr)
+ }
+ }
+
+ // 7. Persist Notes
+ if (parameters.note) {
+ for (Object noteMsg : parameters.note) {
+ String noteStr = String.valueOf(noteMsg)
+ String noteId = delegator.getNextSeqId('NoteData')
+ GenericValue noteData = delegator.makeValue('NoteData', [
+ noteId: noteId,
+ noteInfo: noteStr,
+ noteDateTime: UtilDateTime.nowTimestamp()
+ ])
+ delegator.create(noteData)
+
+ GenericValue orderNote = delegator.makeValue('OrderHeaderNote', [
+ orderId: orderId,
+ noteId: noteId,
+ internalNote: 'N'
+ ])
+ delegator.create(orderNote)
+ }
+ }
+
+ result.orderId = orderId
+ } catch (Exception e) {
+ Debug.logError(e, 'Error creating order via commerce-api: ' + e.getMessage(), 'OrderServices')
+ return ServiceUtil.returnError('Error creating order: ' + e.getMessage())
+ }
+
+ return result
+}
+
+Map commerceGetOrder() {
+ Map result = ServiceUtil.returnSuccess()
+
+ String orderId = parameters.orderId
+ GenericValue orderHeader = from('OrderHeader').where('orderId', orderId).queryOne()
+ if (!orderHeader) {
+ return ServiceUtil.returnError('Order not found with ID: ' + orderId)
+ }
+
+ OrderReadHelper orh = new OrderReadHelper(orderHeader)
+
+ result.putAll([
+ orderId: orderHeader.orderId,
+ externalId: orderHeader.externalId,
+ orderName: orderHeader.orderName,
+ orderTypeId: orderHeader.orderTypeId,
+ salesChannelEnumId: orderHeader.salesChannelEnumId,
+ orderDate: formatDateTime(orderHeader.orderDate),
+ entryDate: formatDateTime(orderHeader.entryDate),
+ priority: orderHeader.priority,
+ currencyCode: orderHeader.currencyUom,
+ status: orderHeader.statusId
+ ])
+
+ // 1. Customer (reusing existing getPartyEmail and getPartyTelephone services)
+ GenericValue placingCustomer = orh.getPlacingParty()
+ if (placingCustomer) {
+ Map customerMap = [:]
+ GenericValue party = from('Party').where('partyId', placingCustomer.partyId).queryOne()
+ customerMap.customerExternalId = party?.externalId
+ if (placingCustomer.getEntityName() == 'Person') {
+ customerMap.firstName = placingCustomer.firstName
+ customerMap.lastName = placingCustomer.lastName
+ } else if (placingCustomer.getEntityName() == 'PartyGroup') {
+ customerMap.lastName = placingCustomer.groupName
+ }
+
+ // Email via getPartyEmail service
+ Map emailResult = runService('getPartyEmail', [partyId: placingCustomer.partyId, contactMechPurposeTypeId: 'PRIMARY_EMAIL'])
+ if (ServiceUtil.isSuccess(emailResult) && emailResult.emailAddress) {
+ customerMap.email = emailResult.emailAddress
+ } else {
+ emailResult = runService('getPartyEmail', [partyId: placingCustomer.partyId, contactMechPurposeTypeId: 'ORDER_EMAIL'])
+ if (ServiceUtil.isSuccess(emailResult) && emailResult.emailAddress) {
+ customerMap.email = emailResult.emailAddress
+ }
+ }
+
+ // Phone via getPartyTelephone service
+ Map phoneResult = runService('getPartyTelephone', [partyId: placingCustomer.partyId, contactMechPurposeTypeId: 'PRIMARY_PHONE'])
+ if (ServiceUtil.isSuccess(phoneResult) && phoneResult.contactNumber) {
+ List phoneParts = []
+ if (phoneResult.countryCode) {
+ String cc = phoneResult.countryCode
+ if (!cc.startsWith('+')) {
+ cc = '+' + cc
+ }
+ phoneParts << cc
+ }
+ if (phoneResult.areaCode) {
+ phoneParts << phoneResult.areaCode
+ }
+ if (phoneResult.contactNumber) {
+ phoneParts << phoneResult.contactNumber
+ }
+ customerMap.phone = phoneParts.join('-')
+ }
+ result.customer = customerMap
+ }
+
+ // Helper closure to format address (returns empty map & uses ternary to solve CodeNarc warnings)
+ Closure