Relation to command/query API
The enfore platform's core API is a command/query API that is based on the CQRS pattern (see Martin Fowler - CQRS). That API provides access to the full MetaCompany domain model. As the MetaCompany model is designed to cover almost any conceiveable use case, the API is very fine-grained and rather complex.
Additionally, the MetaCompany API uses a proprietary definition language and custom tooling for code generation. Both of which cannot easily be used by external developers.
To provide middleware developers with an easy-to-understand, easy-to-use API for integration, we decided to provide the Staff & Access Rights API as an API specialized for Staff & Access Rights workflows/uses cases and to use industry-standard technologies for the definition of the API.
Technology and tooling
The enfore API for integrations is being defined using the OpenAPI specification.
The API will be RESTful and follow industry-standard best practices such as Zalando - RESTful API Guidelines.
Cross-cutting concerns
Cross-cutting concerns affect many of our APIs.
Login Flow
For authorization, we use HTTP basic authentication to authenticate and then give an access token to the APIs.
The access for a given application to various APIs and organisations at the moment cannot be managed by the end-user and are instead managed by the Enfore team. Similarly, application creation is managed by the Enfore team.
Once an application is created and has permissions for an organization, a secret key is generated.
Get Organisation specific access token 'org-access-token'
Calls to the authorize endpoint can be made via the Basic
authorization scheme. For this, you will need to make a GET
call to the /authorize
endpoint with an Authorization
header that contains the text Basic
followed by Base 64
encoded app-name:secret
pair.
For example, if your app-name is myapp
and secret is mysecret
, you will attach the header with value
Basic bXlhcHA6bXlzZWNyZXQ=
where bXlhcHA6bXlzZWNyZXQ=
is the Base64 encoded string containing the
text myapp:mysecret
.
Now you can obtain an org-access-token for the organisation on behalf of which you would like to make calls to our APIs.
When the org-access-token expires, you can repeat the same process to get a new one. Here is an example call for the above request:
curl -X GET \
https://external.apis.enfore.com/auth/authorize/<organisation-id> \
-H 'Accept: */*' \
-H 'Authorization: Basic <base64 encoded app-name and secret>'
The API will then respond with an org-access-token that is valid only for the given organisation ID of which was
provided in the request (note: take a look at organisation-id
variable in the path):
{
"accessToken": <org-access-token>
}
Make API calls
Once you have an org-access-token (specific to the organisation), it can then be used to make the calls to an API via Bearer Authorization header.
Here is an example for the ERP invoices API (Note
curl -X GET \
"https://external.apis.enfore.com/erp/org/<organisation-id>/invoices?from=2019-02-13T00:04:00.000Z&to=2020-05-13T23:59:00.000Z&limit=10&offset=0" \
-H 'Authorization: Bearer <org-access-token>'
Webhooks
Key data
This section covers key data elements used by enfore that are used across multiple APIs.
Platform-defined Tags
Assortment Tags
Assortment tags control what sales channel a product is "listed on". The enfore platform allows users to assign any number of such tags to each product and to each sales channel. A product is considered listed on a sales channel when the intersection of the product's and the sales channel's assortment tags is not empty.
Most assortment tags will be defined by the organization itself and must thus be created by the user via the client UI or via API.
The enfore platform defines only one platform-level assortment tag. This "Default" assortment tag is automatically suggested to user when creating a new product or sales channel via the enforePOS UI. This causes all newly created products to automatically be listed on all newly created sales channels when the user creates them via the enforePOS UI and does not manually assign other tags. Note that the default tag is not automatically assigned when creating products or channels via the API.
The ID of the "Default" assortment tag is: 548c522b3dbfea7cad435741
The full AssortmentTagReference
for the "Default" assortment tag is:
{
"@type": "AssortmentTagReference",
"scope": "PLATFORM",
"id": "548c522b3dbfea7cad435741"
}
Production Tags
Production tags control what processing location a product can be "produced by". The enfore platform allows users to assign a single such tag to each product and one or multiple such tags to each processing location. A product is considered to be "produceable by" a location when the product's production tag is contained in the location's list of production tags.
Most production tags will be defined by the organization itself and must thus be created by the user via the client UI or via API.
The enfore platform defines only one platform-level production tag. This "Default" production tag is automatically suggested to user when creating a new BTO product or processing location via the enforePOS UI. This causes all newly created BTO products to automatically be producable by all newly processing locations when the user creates them via the enforePOS UI and does not manually assign other tags. Note that the default tag is not automatically assigned when creating products or locations via the API.
The ID of the "Default" production tag is: 6139c6e23b1750b9f9963ede
NOTE: Production tags are not yet exposed via API.
Storage Tags
Storage tags control what storage locations a product is "associated with". Associating a product with a storage location is required for the location to be able to hold stock for the product as well as for recording manual stock modifications for the product/location pair. The enfore platform allows users to assign a single such tag to each product and one or multiple such tags to each storage location. A product is considered to be "associated with" a location when the product's storage tag is contained in the location's list of storage tags.
Most storage tags will be defined by the organization itself and must thus be created by the user via the client UI or via API.
The enfore platform defines only one platform-level storage tag. This "Default" storage tag is automatically suggested to user when creating a new inventory product or storage location via the enforePOS UI. This causes all newly created inventory products to automatically be associated with all newly storage locations when the user creates them via the enforePOS UI and does not manually assign other tags. Note that the default tag is not automatically assigned when creating products or locations via the API.
The ID of the "Default" storage tag is: 548c522b3dbfea7cad435742
The full StorageTagReference
for the "Default" storage tag is:
{
"@type": "StorageTagReference",
"scope": "PLATFORM",
"id": "548c522b3dbfea7cad435742"
}
Introduction (Spec)
This documentation describes the high-level architecture of the enfore Contacts-API.
The enfore Contacts-API is provided by the enfore platform to allow creating, reading and updating of contacts with their information.
The API consists of multiple modules that handle
- Individual Contacts
- Professional Identities of Individual Contacts
- Roles of Individual Contacts
- Organization Contacts
- Roles of Organization Contacts
- Customer Lists
- Placeholder Customers
Introduction (Spec)
This documentation describes the high-level architecture of the enfore ERP-API.
The enfore ERP-API is provided by the enfore platform to be convenient to use for ERP use cases in the "Retail" vertical, for example when integrating with an external ERP system such as SAP or Dynamics.
The API consists of multiple modules that allow
- import of master data for sales items, staff members and other business objects
- notification about and querying of invoice data, credit memo data and other business information
The exact set of modules and their capabilities will evolve over time, the initial API version will only support a very limited set of use cases.
Use cases
The aim of the ERP-API is to be useful for implementing use cases related to ERP, specfically in the "Retail" vertical.
Initially, the API will only support a small set of use cases:
- Import and update of sales item master data
- Notification about and fetching of invoice information
Import and update of sales item master data
A main use case for the ERP-API is the import and update of sales item master data. The use case assumes that the external system (e.g., SAP, Dynamics) has ownership of the data and that the enfore platform is subordinate to the external system.
Therefore, when an update request conflicts with a change made on the enfore platform's side, the enfore-side change will be overruled by the external change.
Notification about and fetching of invoice information
Another core use case is based on the usage of the enforePOS devices as a cash registers. Sales performed via the enforePOS devices result in invoice data being generated in the enfore platform (cf. MetaCompany "Ledger" module). The invoice data generated in the enfore platform must then be copied into the external ERP system.
For that, the ERP-API will provide the functionality to subscribe to an "invoice data available" event as well as a way to fetch the new invoice data.
Notification about and fetching of credit memo information
Another core use case is based on the usage of the enforePOS devices as a cash registers. Voids or sales returns performed via the enforePOS devices result in credit memo data being generated in the enfore platform (cf. MetaCompany "Ledger" module). The credit memo data generated in the enfore platform must then be copied into the external ERP system.
For that, the ERP-API will provide the functionality to subscribe to an "credit memo data available" event as well as a way to fetch the new credit memo data.
Domain model
The domain model of the ERP-API consists of the core object types Product
, Invoice
and CreditMemo
. Other objects such as Customer
and Staff
are part of different APIs. In essence, Staff
sells Products
to Customers
and each sale is recorded as an Invoice
. Voids or sales returns are recorded as CreditMemo
.
Product
A Product
represents an item that is being sold by the business.
Each product has a name
and various identifiers. The id
is the technical identifier by which the enfore platform addresses the product. The external_id
is an identifier that can be set via the ERP-API and that allows integrators to address the product directly without the need for a lookup. The article_id
is the identifier by which the business communicates with the customer (aside from he product's name
), it is shown on invoice documents, for example. Further identifiers can be stored as part of the product_identifiers
structure, for example EAN and GTIN numbers.
Each product must have a management_unit
. This unit, combined with the numeric value one, defines the minimum quantity of the product that can be addressed. For example, a product using MASS_KILOGRAMS
as management unit can only be addressed in integer multiples of 1kg
. It would not be possible to have a sale, purchase or stock change of 1.5kg
or 500g
of the product.
As products are to be sold, they also need a sales_quantity
and a sales_price
. The sales_quantity
specifies the default quantity for a sale and the sales_price
specifies the price for that default quantity of the product. For example, given product with the sales quantity of 5kg
and the sales price of 10€
, a customer purchasing 20kg
of said product would need to pay 40€
. The flag sales_price_is_gross
indicates whether the sales_price
is a net amount (when sales_price_is_gross == false
) or a gross amount (when sales_price_is_gross == true
).
For computing VAT/sales tax, each product must define its tax_category
. Additionally it may define a reason for it to be exempt from taxation via its tax_exemption_reason
field.
Product relationships
NOTE: The ERP-API currently does not provide a way to declare or query product relationships. This functionality will be added in the future. But as the enforePOS client allows creation of such relationships, the ERP-API must be able to represent the resulting invoice item dependencies and for understandng those, the following documentation was added.
The enfore platform allows modelling of relationships between products. Different types of relationships exist, and they influence the behavior of the platform in different ways.
Related products
A very simple form of product relationship is the "related product". When a product B is configured as a "related product" to product A, the register UI will suggest a sale of product B whenever product A is placed into the basket.
The "related product" relationship allows specifying a quantity and price for the linked product that differ from its regular sales quantity and price. This enables easy cross-selling products at a discount.
Options and option groups
When a product is configurable for a specific sale, it is often convenient to not model all possible configurations as separate products but to model the "core product" and its "options". When options are mutually exclusive, they can be wrapped in an "option group". Selection from a group may then also be set to mandatory.
For example, a product "Flat White" represents a coffee that does contain milk but may provide the choice between regular and soy milk. This would ideally be modeled as three products "Flat White", "Regular Milk" and "Say Milk" with the "Flat White" product having an option group "Milk" that referenced the other two products as "options" and whose "selection is mandatory" flag would be set.
If the "Flat White" additionally allowed the choice of having chocolate sprinkles on top, a fourth product "Chocolate Sprinkles" could be defined and the "Flat White" would reference that as an "option".
An "option" always references a product and may have a quantity/price that overrride that products regular sales quantity/price. It also has a flag controlling whether the option can be selected multiple times.
An "option group" bundles multiple "options" under a user-defined label. It has flags that control whether selection from the group is mandatory or optional and whether multiple or only one option from the group can be choosen.
Deposits
Deposit relationships represent "bottle deposit" that the customer has to pay when purchasing an item. For example, a when selling a drink in a disposable bottle in Germany, the merchant is required to collect a deposit of 0.25€. The customer can get the deposit refunded when she returns the empty bottle to the merchant later.
This usually is modeled by defining two products. One for the drink and another one for the deposit. The drink product would then reference the deposit product via a "deposit" relationship.
Custom workflows
The enfore platform provides extension points where the regular behavior of the platform can be extended and/or overridden by custom logic. This feature is known as "custom workflows". The API allows setting the start URLs for the product-related custom workflows via the field custom_workflow_config
.
For more information regading custom workflows, please see CUWO architecture and interfaces (internal link).
Invoice
An Invoice
represents the financial result of a sale.
It consists of various context information such as the store where the sale took place (via store_id
), what register was used (via register_id
) and which staff member handled the sale (via operator_id
). The date and time of the sale is recorded as transaction_date_and_time
.
Each invoice has a sequence of items
that hold information about the products that were sold. Each item specifies the product that was sold, the sold quantity, the net/gross amounts, and any applied taxes and item-level discounts.
An invoice also has sequences of discounts
and payments
. The former hold information about basket-level discounts and the latter hold information about the payments made by the customer.
Edge cases
While ERP-API is focussed on the "retail" vertical and tries to provide a domain-specific view on the business data, the enfore platform provides some advanced features that cannot easily be mapped to this domain-specific view.
Invoices resulting from split/merged orders
The enfore platform uses "order" objects to represent sales. In the retail vertical, an "order" can be seen as to be equivalent to the list of items shown on the cash register. An order itself does not represent a sale yet, as it can be still be changed and/or aborted. Usually, once the merchant and customer agree on the sale, the order is converted into an invoice.
Nevertheless, the enfore platform allows invoices to be created for only part of an order, for a combination of multiple orders, or for a combination of both. For example, in the "gastro" vertical, it is common to use "one order per table" as this allows dishes to be served together and easy hand-over between waiters on shift changes. When creating invoices, it then often happens that each person/couple at the table requests a separate invoice. Or that a larger group of people is split accross multiple tables but then requsts a single, combined invoice.
When an invoice does not represent a single, complete order anymore, some additional definitions are needed to reason about the fields in the ERP-API:
- The values for
store_id
,register_id
,transaction_date_and_time
, andoperator_id
, are where, when and by whom the combined/split invoice was created. - The values for
transaction_start
andtransaction_end
are the earliest start/lastest end of all orders that have at least one item included in the invoice. - When combining/splitting orders, discounts may be recomputed. Whether discounts are computed on the invoice- or order-level depends on the sale process. For example, "gastro" orders may be merged and then discounts are recomputed based on the combined invoice. For "online" orders, there may be partial invoices caused by partial shipments but discounts are still computed based on the original order.
Late payments
The ERP-API allows clients to be notified about new invoices and when fetched, those invoices include information about the payment(s) made by the customer.
The enfore platform provides a feature called "pay later" where an invoice is created and booked but payment is deferred to a later point in time. In such cases, the ERP-API will send a notification about the new invoice, but when the invoice is fetched before the payment took place, the returned Invoice
object will not include the payment information.
The ERP-API currently does not provide "payments" as a separate object type or notifications about "payments".
InvoiceItem
An InvoiceItem
represents a single position in an invoice.
It references a product via its product_id
and product_external_id
. The quantity of product that was sold is recorded in the item's quantity
field. The total amount of the item is available via gross_amount
and net_amount
.
Information about item-level discounts is available as discounts
sequence and the undiscounted amounts of the item are stored in undiscounted_gross_amount
and undiscounted_net_amount
.
Information about taxes is available as taxes
sequence. A sequence is used here as multiple taxes might be applied to a single item, for example US sales tax usually consists of a general state sales tax and local or city sales taxes. In those cases, a single InvoiceItem
may have multiple TaxInformation
elements.
Dependencies between items
An invoice item can depend on other invoice items. Dependencies on invoice items are caused by relations between products. For example, when a product A that has a related deposit product B, the invoice item for A will also have a dependant invoice item for B.
See the section about product relationships for more information.
CreditMemo
A CreditMemo
represents the financial result of a sales void or sales return.
It is structurally almost equal to an Invoice
with the exception that there are no discounts.
CreditMemoItem
A CreditMemoItem
represents a single position in a credit memo.
It is structurally almost equal to an InvoiceItem
with the exception that there are no discounts or item dependencies.
Customer
See the contacts API.
Staff
See the staff members & access rights API.
Product features
In the enfore platform, products have various properties that are explicitly modeled in the Product
data structure and its subresources. For example, the product's name, management unit, sales price/quantity, etc.
Naturally, not all properties of every product in the world can be modeled. Trying to do this would work against the goal of the "MetaCompany" model being suitable to all kinds of businesses. Therefore, the enfore platform has a generic way to express custom product properties by the way of "product features".
In short, "product features" allow custom properties to be declared by the merchant for any of his products. Those custom properties can be organized in various ways and can be configured to be shown in different UI contexts as needed by the merchant.
The domain model for "product features" mainly consists of the data types FeatureTemplate
, ProductFeature
and various types of FeatureValue
/FeatureValues
.
A FeatureTemplate
exists independent of any Product
and describes a custom property (without actually assigning it to any Product
yet). The template has a name, type, value type, a list of possible values and various additional information. For example, a tailer shop might define a feature template named "Color" with the type COLOR
and the possible values of "Red"
, "Green"
, and "Black"
. And it might also define a feature template named "Arm length" with the type SIZE
and the possible "length" values of 30cm
and 26cm
.
Note that the "possible values" declared as part of a FeatureTemplate
consist of both the definition of the value type and the definition of the actual values. The type is mandatory and static but the actual values may change over time.
In addition to the type
, feature templates can also be organized by category
and group
. Categories are predefined by enfore (currently only SPECIFICATIONS
and INGREDIENTS
exist) whereas groups can be defined by the merchant (although enfore does provide a couple of pre-defined groups, e.g., "allergens" or "dietary information").
Once a feature template has been defined, a Product
can declare a ProductFeature
to define that "this product has that feature/that custom property". The ProductFeature
references a FeatureTemplate
and (optionally) specifies a value (from the ones allowed by the template). E.g., the "Green Shirt" product may have the features "Color" with value "Green"
and "Arm length" with value 26cm
.
The value in the ProductFeature
is optional so we can support some advanced use cases:
- Products with variants (where the variants are implicitly defined by feature value combinations)
E.g., Product "T-Shirt" with variant-defining features "Color" (values red, green, blue) and "Size" (values S, M, L) would have the 6 variants red/S, red/M, red/L, green/S, green/M, green/L, blue/S, blue/M, and blue/L - Products with feature values that differ by Lot
E.g., Product "Red Roses" with feature "Stem length" defined "per Lot"
Feature types & values
Each product feature must be assigned a type
and a value_type
. Most also have a list of possible values (in the template) and a value (in the product/lot).
Since product features are effectively object properties, they always have some kind of value, at least from an abstract interpretation.
Take for example a product like a "Shopping bag". It could have the following properties:
- Material = Cotton
- Base color = Grey
- Highlight color = Red
- Weight = 350g
- Volume capacity = 10l
- Weight capacity = 15kg
- Size = M
- Food safe = yes
- Washable = yes
The type
of a feature defines the semantic type of the feature's value. The following feature types exist:
Feature type | Description |
---|---|
CAPACITY | Feature describes a capacity. E.g., the bag's volume or weight capacity. |
COLOR | Feature describes a color. E.g., the bag's base and highlight color. |
FORM_FACTOR | Feature describes a form factor. E.g., the bags shape. |
TAG | The feature serves a label. E.g., the "food safe" or "washable" lables on the bag. |
MATERIAL | Feature describes a material. E.g., the bag's material. |
PERFORMANCE | Feature describes a performance characteristic. E.g., a car's speed or a pump's throughput. |
SIZE | Feature describes a size. E.g., the bag's size or weight. |
OTHER | The "catch-all" type if none of the others match. |
The value_type
of a feature defines the technical type of the feature's value. The following feature value types exist:
Feature value type | Description |
---|---|
ABSTRACT | The value is a string |
BOOLEAN | The value is a boolean . |
INTEGER | The value is an integer number . |
NUTRITION_DECLARATION | The value is a NutritionDeclaration . |
QUANTITY_DIGITAL_STORAGE | The value is a Quantity with the unit being one of the DIGITALINFORMATION_… units. |
QUANTITY_ENERGY | The value is a Quantity with the unit being one of the ENERGY_… units. |
QUANTITY_LENGTH | The value is a Quantity with the unit being one of the LENGTH_… units. |
QUANTITY_MASS | The value is a Quantity with the unit being one of the MASS_… units. |
QUANTITY_PIECES | The value is a Quantity with the unit being QUANTITY . |
QUANTITY_VOLUME | The value is a Quantity with the unit being one of the VOLUME_… units. |
TEXT | The value is a longer text as a string |
NONE | The feature has no value. |
Note that the feature type limits the allowed feature value types:
Feature type | Allowed feature value types |
---|---|
CAPACITY | ABSTRACT , INTEGER , QUANTITY_PIECES , QUANTITY_LENGTH , QUANTITY_VOLUME , QUANTITY_MASS , QUANTITY_ENERGY , QUANTITY_DIGITAL_STORAGE |
COLOR | ABSTRACT |
FORM_FACTOR | ABSTRACT |
MATERIAL | ABSTRACT |
PERFORMANCE | ABSTRACT |
SIZE | ABSTRACT , INTEGER , QUANTITY_PIECES , QUANTITY_LENGTH , QUANTITY_VOLUME , QUANTITY_MASS , QUANTITY_ENERGY , QUANTITY_DIGITAL_STORAGE |
TAG | NONE |
OTHER | ABSTRACT , BOOLEAN , INTEGER , QUANTITY_PIECES , QUANTITY_LENGTH , QUANTITY_VOLUME , QUANTITY_MASS , QUANTITY_ENERGY , QUANTITY_DIGITAL_STORAGE , TEXT |
Feature categories & groups
To organize features, merchants can use feature categories and feature groups. Both are use to determine which features are displayed where in the UI. For example, the product info view will use a tab for each category and then group the features of each tab by feature group.
Feature display configuration
Giving merchants the option to declare custom properties for their products is only useful if those custom properties are also visible in the client UI and/or on business documents.
For this, each FeatureTemplate
and ProductFeature
provide a "display configuration".
A FeatureDisplayConfiguration
allows the merchant to choose an icon (from the list of built-in icons) and a color (from the list of defined "highlight colors") and specfiy a list of "UI contexts" in which the feature shall be shown.
The configuration on the FeatureTemplate
serves as a blueprint for the configuration on the ProductFeature
. It is copied from the FeatureTemplate
on creation of the ProductFeature
but is then "detached" from it (i.e., the configuration on the ProductFeature
doesn't update when the configuration on the related FeatureTemplate
is modified). For the actual display, only the configuration on the ProductFeature
is taken into account.
Icons
The enfore platform defines a right set of icon to choose from.
Since there are too many icons to list them all here (and the icon set gets extended all the time), please refer to the source repo.
Colors
The enfore platform currently defines six highlight colors to choose from:
GLUE
GOLDEN
PURPLE
TEAL
YELLOW
When no specific color is chosen by the merchant, a generic fallback color is automatically used.
Display contexts
The enfore platform defines the following display contexts for product features:
Context | Description |
---|---|
PRODUCT_LISTING_IN_REGISTER | Product selection views (list/card modes only) in the register. |
LINE_ITEMS_IN_ACTIVE_ORDERS | Line items in orders in the register. |
LINE_ITEMS_IN_ORDER_MANAGEMENT_AND_HISTORY | Line items in orders in sales history and order management views. |
PRODUCT_INFO_VIEW | Product info view in register an invoicing. |
SALES_ORDER_BUSINESS_DOCUMENTS | Line items in business documents. |
Browse Nodes
The enfore platform provides various places where users and/or customers can view the organizations products. Aside from search, and product lists, the primary way to navigate products is via "browse nodes".
Browse nodes are defined by the organization (vie API or client UI) and form an acyclic graph that can be navigated. Each node in the graph is called a browse node and any single product might be associated with any number of browse nodes.
For example, an electronics store selling gaming consoles might create a browse node graph like this:
A single product can be assigned to any number of browse nodes. For example, the Xbox version of the video game "F1 2020" may be assiged to XBox > Games > Sports
and XBox > Games > Racing
.
Note that it is possible to assign products to non-leaf browse nodes. For example, a game that doesn't fit into one of Action
, Racing
, Sports
or Family
could be directly assigned to Games
Order of child nodes for a browse node
Browse nodes form a directed acyclic graph via their parent->child relationships. For purposes of displaying child nodes of a specific parent to the user, an order over the children of a parent is necessary.
That order can be explicitly contolled via the /org/{org-id}/browse-nodes/{browse-node-id}/children
endpoint.
When modifying the list of children indirectly (via PUT
on a specific node with a different set of parents or via POST
of a new node), new children are always appended to the existing list of children.
Product variants
Products often come in different variants. For example, a specific harddisc model exists with different storage capacities. Or a type of trousers comes in different sizes and colors.
To translate this into the enfore platform, each variant is expressed as a product ((Inventory)Product
, BTOProduct
, StaticSetProduct
) and all those products are linked to a ProductVariantGroup
.
For example, the "980 PRO NVMe M.2 SSD" product family could be expressed as
While the above example shows all variants having the same set of features, this isn't a requirements. It is possible to have variants with different sets of features or even without any features at all.
Note that is is also possible to have variant groups consist of products of different types (Inventory, BTO, Static Set)
Defining product variant groups
Before a product can be assigned to a product variant group, the group first has to be created via POST
against /org/{org-id}/product-variant-group
. Trying to assign a product to a non-existent variant group will fail with a 422
error.
Operations batches
The ERP-API provides "operations batches" that allow clients to upload a whole set of operations of the same type to be executed as a batch. This allows for a controlled execution of thousands of updates without needing an independent HTTP call for each operation.
The basic concept is that clients create a ZIP file containing all the single operations that they want to have executed as part of the batch, then request an "S3 upload URL" via GET /org/{org-id}/operations-batches/upload-url
, then upload the ZIP file to that URL, and then trigger creation and execution of the batch via a call to POST /org/{org-id}/operations-batches/XXX
(where XXX
depends on the type of batch).
Batch payload ZIP file
The single operations included in an operations batch are to be provided as a ZIP file. Within the ZIP file, a single JSON file is expected for each single operation. The name of the file must be the ID of the operation followed by .json
.
Operations are executed in the order of their IDs, sorted as ASCII ascending.
For example, an '"valid from"-price update' batch that aims to change the future prices for a single product on three different sales channel as well as the base price of the product would need to consist of four operations:
// Product base prices
{
"@type": "ProductValidFromPriceUpdate",
"operation_id": "no_channel",
"product": {…},
"price_list": […]
}
// Product prices for sales channel 1
{
"@type": "ProductSalesChannelValidFromPriceUpdate",
"operation_id": "c1",
"product": {…},
"sales_channel": {
"id": "61bc6aa66e5ecde3f5caa2cd",
"type": "POS_SALES_CHANNEL"
},
"price_list": […]
}
// Product prices for sales channel 2
{
"@type": "ProductSalesChannelValidFromPriceUpdate",
"operation_id": "c2",
"product": {…},
"sales_channel": {
"id": "61bc6aab98ad9ead7d332972",
"type": "POS_SALES_CHANNEL"
},
"price_list": […]
}
// Product prices for sales channel 3
{
"@type": "ProductSalesChannelValidFromPriceUpdate",
"operation_id": "c3",
"product": {…},
"sales_channel": {
"id": "61bc6aaf317c902434238bbd",
"type": "POS_SALES_CHANNEL"
},
"price_list": […]
}
Therefore, the ZIP file is expected to contain the four files:
no_channel.json
c1.json
c2.json
c3.json
The operations would then be executed in the order c1
, c2
, c3
, no_channel
.
The ZIP file must be a basic ZIP file that does not use any advanced features like encryption, volume spanning/segmentation, digital signatures or compression algorithms other than DEFLATE. At the minimum, the file must be fully compatible with the Java ZipInputStream
implementation.
Types of operations batches
Each type of batch allows only a limited set of operation types as its payload. This is done so the enfore platform can optimize execution of the operations by, for example, using different scheduling and/or caching options for each type of batch. Additionally, having only operations of related type on one batch helps users when viewing the list of active/complete batches.
"Valid from"-price updates
A batch of type "Valid from"-price updates supports two types of operations:
ProductValidFromPriceUpdate
- Update (or remove) the list of "valid from"-base prices for a productProductSalesChannelValidFromPriceUpdate
- Update (or remove) the list of channel-specific "valid from"-prices for a product
ProductValidFromPriceUpdate:
ProductValidFromPriceUpdate:
type: object
properties:
"@type":
type: string
enum: ["ProductValidFromPriceUpdate"]
operation_id:
type: string
minLength: 1
maxLength: 256
product:
$ref: 'product_shared.yaml#/components/schemas/ProductReference'
price_list:
$ref: 'product_valid_from_prices.yaml#/components/schemas/ValidFromPricesList'
required:
- "@type"
- operation_id
- product
ProductSalesChannelValidFromPriceUpdate:
ProductSalesChannelValidFromPriceUpdate:
type: object
properties:
"@type":
type: string
enum: ["ProductSalesChannelValidFromPriceUpdate"]
operation_id:
type: string
minLength: 1
maxLength: 256
product:
$ref: 'product_shared.yaml#/components/schemas/ProductReference'
sales_channel:
$ref: 'product_shared.yaml#/components/schemas/SalesChannelReference'
price_list:
$ref: 'product_valid_from_prices.yaml#/components/schemas/ValidFromPricesList'
required:
- "@type"
- operation_id
- product
- sales_channel
Appendix A - Key data
This section covers key data elements used by enfore. Most of those are reused for multiple data structures.
Country
We use ISO 3166 Alpha-2 codes for denoting countries. We support all currently defined countries.
Currency
We use ISO 4217 codes for denoting currencies. We currently can store the following currencies:
Code | Name |
---|---|
AUD | Australian Dollar |
BRL | Brazil Real |
CAD | Canada Dollar |
CHF | Schweizer Franken |
CNY | China Yuan Renminbi |
DKK | Danmark Øre |
EUR | Euro |
GBP | United Kingdom Pound |
HKD | Hongkong Dollar |
IDR | Indonesia Rupiah |
INR | Indien Rupie |
JPY | Japan Yen |
KRW | Republic (South) Korea Won |
MXN | Mexico Peso |
MYR | Malaysia Ringgit |
NOK | Norwegen Øre |
NZD | New Zealand Dollar |
PHP | Philippinen Peso |
RUB | Russia Ruble |
SEK | Sweden Krone |
SGD | Singapur Dollar |
TRY | Turkey Lira |
TWD | Republik China (Taiwan) Dollar |
USD | United States Dollar |
ZAR | South Afrika Rand |
TaxCategory
We assign a single "tax category" to each sales item to control how sales of that item are taxed. The tax categories are locale independent and do not represent an actual tax rate directly. Rather, this is handled by "tax rules" that are configured in the platform. A "tax rule" defines conditions and references an actual "tax". The "tax" objects are the actual locale-specific taxes with their rate.
The platform currently defines the following tax categories:
Key | Name | Internal enfore-ID |
---|---|---|
REDUCED | Reduzierte Steuer | 58b1978e9ffc03bb333dee00 |
REDUCED_TAKE_AWAY | Reduzierte Steuer (außer Haus) | 58b1978e9ffc03bb333dee04 |
REDUCED_SPECIAL | Reduzierte Steuer (speziell) | 58b1978e9ffc03bb333dee02 |
STANDARD | Standardsteuer | 58b1978e9ffc03bb333dee03 |
NO_TAX | Keine Steuer | 58b1978e9ffc03bb333dee01 |
TaxExemptionReason
For product where no VAT/sales tax is to be applied on a sale, a tax exemption reason must be given.
Key | Used for |
---|---|
UNIVERSAL_POSTAL_SERVICE_DE | Exemption for universal postal services; in Germany following §4 11b UStG. Some conditions need to be fulfilled to be able to claim that tax exemption. |
INTRA_COMMUNITY_TRADE_EU | Delivery of tangible goods shipped from one country to another country of the EU. These deliveries are exempt from VAT in the country of departure of the goods if some conditions are met. |
THIRD_PARTY_COUNTRY_DELIVERY_EU | Delivery of tangible goods shipped from one country to another country outside of the EU. These deliveries are exempt from VAT in the country of departure of the goods. |
REVERSE_CHARGE_EU | When you buy goods or services from suppliers in other EU countries, the Reverse Charge moves the responsibility for the recording of a VAT transaction from the seller to the buyer for that good or service. |
UnitOfMeasure
For denoting the unit of measure for a Quantity value, we use a fixed set units split by type (e.g., length, mass, volume). We currently support the following units of measure:
Digital information
Type | Unit |
---|---|
DIGITALINFORMATION | BYTES |
DIGITALINFORMATION | KILOBYTES |
DIGITALINFORMATION | KIBIBYTES |
DIGITALINFORMATION | MEGABYTES |
DIGITALINFORMATION | MEBIBYTES |
DIGITALINFORMATION | GIGABYTES |
DIGITALINFORMATION | GIBIBYTES |
DIGITALINFORMATION | TERABYTES |
DIGITALINFORMATION | TEBIBYTES |
DIGITALINFORMATION | PETABYTES |
DIGITALINFORMATION | PEBIBYTES |
Energy
Type | Unit |
---|---|
ENERGY | JOULES |
ENERGY | GIGAJOULES |
ENERGY | MEGAJOULES |
ENERGY | KILOJOULES |
ENERGY | CENTIJOULES |
ENERGY | MILLIJOULES |
ENERGY | MICROJOULES |
ENERGY | CALORIES |
ENERGY | KILOCALORIES |
ENERGY | WATTHOURS |
ENERGY | GIGAWATTHOURS |
ENERGY | MEGAWATTHOURS |
ENERGY | KILOWATTHOURS |
ENERGY | CENTIWATTHOURS |
ENERGY | MILLIWATTHOURS |
ENERGY | MICROWATTHOURS |
Length
Type | Unit |
---|---|
LENGTH | KILOMETERS |
LENGTH | METERS |
LENGTH | DECIMETERS |
LENGTH | CENTIMETERS |
LENGTH | MILLIMETERS |
LENGTH | POINTS |
LENGTH | INCHES |
LENGTH | FEET |
LENGTH | YARDS |
Mass
Type | Unit |
---|---|
MASS | TONS |
MASS | KILOGRAMS |
MASS | GRAMS |
MASS | MILLIGRAMS |
MASS | POUNDS |
Quantity
Type | Unit |
---|---|
QUANTITY | PIECES |
Time
Type | Unit |
---|---|
TIME | SECONDS |
TIME | MINUTES |
TIME | HOURS |
TIME | DAYS |
TIME | WEEKS |
TIME | MONTHS |
TIME | YEARS |
Volume
Type | Unit |
---|---|
VOLUME | CUBIC_MILLIMETERS |
VOLUME | CUBIC_CENTIMETERS |
VOLUME | CUBIC_DECIMETERS |
VOLUME | CUBIC_METERS |
VOLUME | MILLILITERS |
VOLUME | CENTILITERS |
VOLUME | DECILITERS |
VOLUME | LITERS |
VOLUME | HECTOLITERS |
VOLUME | FLUID_OUNCES |
VOLUME | PINTS |
VOLUME | QUARTS |
VOLUME | GALLONS |
VOLUME | BARRELS |
Appendix B - Common value object types
There are some common types of value objects that are repeatedly used within the actual business model objects.
Address
An Address represents a street address/postal address.
Field | Value type | Value range |
---|---|---|
street | String | up to 256 Unicode Characters |
streetNumber | String | up to 256 Unicode Characters |
postalCode | String | up to 256 Unicode Characters |
city | String | up to 256 Unicode Characters |
country | String | Country |
Money
A Money represents a monetary value (i.e., a currency and an amount), for example "120 EUR" or "2,500.75 USD".
Field | Value type | Value range |
---|---|---|
amount | String | Numeric value in the range of -9,000,000,000,000 to 9,000,000,000,000 using "." as decimal separator with up to 6 decimal digits. |
unit | String | Currency |
Quantity
A Quantity represents a quantity value (i.e., a unit and an amount), for example "3 pcs" or "2.5 kg".
Field | Value type | Value range |
---|---|---|
amount | String | Numeric value in the range of -9,000,000,000,000 to 9,000,000,000,000 using "." as decimal separator with up to 6 decimal digits. |
unit | String/Object | UnitOfMeasure (representation as string or object TBD) |
Introduction (Spec)
The purpose of the External ID Service is to support integrators. It provides means to store mappings between their own external ID and Enfores internal ID. At its current state, it is basically a key/value store.
Mapping
- need to be bijective, that means key/value must be unique in each direction.
- are scoped to:
- organization
- namespace
- entity-class
A namespace must be created once before it's usable. Valid entity-classes are exposed by the external ID service itself.
Entity-classes are e.g.:
- products
- feature-templates
- feature-groups
- invoices
- sales-categories
- lots
- sales-orders
- staff-members
- bank-accounts
Introduction (Spec)
This documentation describes the high-level architecture of the enfore Inventory-API.
The enfore Inventory-API is provided by the enfore platform to be convenient to use for inventory-related use cases in the "Retail" vertical, for example when integrating with an external system such as SAP or Dynamics.
The API consists of multiple modules that allow
- notification about, querying and creation of stock count changes (e.g., items removed from stock due to a sale)
- notification about, querying and creation/update of inventory shipments (e.g., moving items from storage A to storage B)
- notification about, querying and creation/update of goods-in/out (e.g., receiving items from external to storage/moving items from storage to external)
The exact set of modules and their capabilities will evolve over time, the initial API version will only support a very limited set of use cases.
Use cases
Domain model
The domain model of the Inventory-API consists of the core object types InventoryShipment
, GoodsIn
, GoodsOut
, and StockCountChange
.
The inventory management model of the enfore platform is based on a three-level hierarchy:
- At the top level are high-level business operations such as a sale of products to some customer (represented by a
SalesOrder
) or the transfer of inventory stock from one location to another (represented by anInventoryShipment
). - The middle level are unidirectional inventory movements of one or multiple products (represented by
GoodsIn
andGoodsOut
objects) - At the lowest level are the actual changes to stock count (represented by
StockCountChange
objects).
Objects at the top and middle level usually have some lifecycle. This allows such objects to be created without immediately having an effect on stock counts (e.g., one can sell a product to a customer but until it has been shipped, it is still part of the stock count). Once created, such objects usually can be rejected, accepted, performed, canceled, and completed. With the exact meaning of those states depending on the type of object.
Objects at the bottom level are purely for bookkeeping. They usually do not have a life cycle but are simple records of what happened.
The distinction between top and middle level is that top-level objects are business operations whereas mid-level objects are a technical abstraction/part of a top-level business operation. For example, an InventoryShipment
is a business operation in which some quantity of some products is to be transferred from one storage location to another. In the enfore platform, this results in two middle-level objects being created. A GoodsOut
for removing the stock from the source location and a GoodsIn
for adding the stock to the target location. This allows staff at both locations to know what is to happend and perform their task. Once staff at the source location has packed and sent out the shipment, they can mark the GoodsOut
as shipped and the system reduce the stock count accordingly (and create StockCountChange
records). But until staff at the target location marks the GoodsIn
as "received", the target location's stock count isn't increased yet.
InventoryShipment
An inventory shipment represents the business operation of transferring some quantity of some products from one storage location to another.
The API provides InventoryShipmentDraft
objects for construcing an inventory shipment. An inventory shipment draft does not have any business impact yet and can freely be changed (positions added/changed/removed, locations updated, etc.) or deleted.
Once a draft is to become active, an InventoryShipment
object must be created. Once created, an inventory shipment must traverse its life-cycle and cannot be deleted (but can be canceled). The creation of an inventory shipment also automatically creates a GoodsOut
for the source location and a GoodsIn
for the target location.
Inventory shipment lifecycle
The lifecycle of an inventory shipment consists of the initial state NEW
, the two final states COMPLETED
and CANCELED
, and the intermediate state IN_PROCESS
:
The meaning of the states is as follows:
State | Meaning |
---|---|
NEW | The inventory shipment has been newly created (along with the associated goods out/in objects). Both associated objects are also still in state NEW |
IN_PROCESS | The inventory shipment is in process, meaning at least one of the associated goods out/in objects is in an intermediate state. |
COMPLETED | The inventory shipment has been completed. That is, both associated goods out/in objects are completed as well. |
CANCELED | The inventory shipment has been canceled. Note that the associated goods out/in objects, can be in almost any state combination except both being completed. |
GoodsOut
A GoodsOut
represents the removal (usually via shipment) of some quantity of some products from a storage location.
Most GoodsOut
objects belong to some high-level business operation such as an InventoryShipment
or SalesOrder
but the platform also allows for "unspecified/external" GoodsOut
operations.
Those are GoodsOut
that are not created by the enfore platform based on high-level business operations but via the client or API.
This is used for cases where the high-level operation isn't known (yet).
For example for inventory shipments where the source location isn't modeled in the enfore platform, e.g., some kind of central enterprise warehouse.
To create an "unspecified/external" GoodsOut
via the API, one first has to create a GoodsOutDraft
and then create a GoodsOut
from it.
Similar to the inventory transfer API, the API for GoodsOutDraft
provides functionality to freely modify/delete it whereas for GoodsOut
, the API enforces a strict lifecycle.
Goods out lifecycle
The lifecycle of a goods out consists of the initial state PENDING
, the two final states COMPLETED
and CANCELED
, and some intermediate states:
The meaning of the states is as follows:
State | Meaning |
---|---|
PENDING | The goods out has not yet happend and the items are not yet prepared either. This is the initial state. |
PREPARED | The items have been prepared for transfer (e.g., packed for shipment/pickup). |
SHIPPED | The items have been shipped / handed over to the shipping carrier. |
COMPLETED | The items have been received at/by the destination location/customer. This is a final state. |
CANCELED | The goods out has been canceled. Items may or may not have been prepared/shipped before the cancelation. This is a final state. |
Goods out types
A goods out has a type
that indicates how the goods are moved. The following types are used by the API:
Type | Meaning |
---|---|
DELIVERY | The items are delivered via shipment |
PICKUP | The items are picked up |
GoodsIn
A GoodsIn
represents the addition (usually via shipment) of some quantity of some products to a storage location.
Most GoodsIn
objects are related to (originate from)some high-level business operation such as an InventoryShipment
or GoodsProcurement
but the platform also allows for "unspecified/independent" GoodsIn
operations.
Those are GoodsIn
that are not created by the enfore platform based on high-level business operations but via the client or API.
This is used for cases where the high-level operation isn't known (yet).
For example when a shipment from a customer is received but the customer hasn't notified the merchant about his intent to return the product yet.
Another example is an external system that dispatches goods from a central warehouse to an organizations branches and where the central warehouse isn't modeled in the enfore platform.
To create an "unspecified/independent" GoodsIn
via the API, one first has to create a GoodsInDraft
and then create a GoodsIn
from it.
Similar to the inventory transfer API, the API for GoodsInDraft
provides functionality to freely modify/delete it whereas for GoodsIn
, the API enforces a strict lifecycle.
Goods-in lifecycle
The lifecycle of a goods-in consists of the initial state PLANNED
, the three final states COMPLETED
, CANCELED
, and REJECED
, and the intermediate state IN_PROCESS
:
The meaning of the states is as follows:
State | Meaning |
---|---|
PLANNED | The goods-in has been created, but no goods have been received yet. |
IN_PROCESS | At least some goods have been received but the goods-in isn't completed yet. For example, goods may still need to be counted/checked. |
COMPLETED | Resolutions for all received goods have been processed. This is a final state. |
CANCELED | The goods-in has been canceled. Goods may have been received and resolved. This is a final state. |
REJECTED | The goods-in has been rejected. No goods have been received. This is a final state. |
StockCountChange
The enfore platform tracks stock count on a "per location + product + condition"-basis. That is, for each triplet of location, product and condition a separate stock count exists. Whenever the one such count changes, a StockCountChange
object is created to record that change.
A StockCountChange
object is only created once the change actually happened.
Stock-taking
Stock-taking is the process of physical verification of the actual quantities and conditions of goods held in a storage location.
Domain model
The enfore platform has support for stock-taking in enforePOS1 and enforePOS2 with a substantially different set of features. Therefore, there are two different domain models, one for enforePOS1 stock-taking and one for enforePOS2 stock-taking.
enforePOS1
An enforePOS1 stock-taking is represented by a StockTaking
structure with a minimal set of configuration options. In enforePOS1, stock-taking is always performed by entering counting data on a resource-by-resource basis. There is no support for concurrent counting using multiple participants nor for counting areas.
The general life-cycle of an enforePOS1 stock-taking is rather simple:
enforePOS2
TBD
Data export
This section describes how data for a stock-taking can be exported via the enfore API and the format of the exported data.
Triggering data export
A data export for a stock-taking can be triggered by a POST /org/{org-id}/stock-taking-exports
with a StockTakingReference
identifying the stock-taking whose data to export as payload. Note that triggering an export is only possible for stock-takings that have reached a terminal state (CANCELED
, COMPLETED
, or COMPLETED_RECONCILIATION
).
The state machine for a data export is simple:
The status of a StockTakingDataExport
can be determined by polling. Once the status reaches COMPLETED
, a download URL for the export data can be acquired via GET /org/{org-id}/stock-taking-exports/{export-id}/download
.
The "happy-path" sequence of exporting data for a stock-taking is:
Export data format
The result of a data export for a stock-taking is a ZIP file containing a JSON file with meta-data about the stock-taking as well as a number of CSV files containing the counting and context data of the stock-taking.
The general structure of the ZIP file is:
CSV format rules
General
All CSV files of the stock-taking export format adhere to the CSV format as defined by RFC4180 and use a header row. The header row will contain the names of the columns as defined in the DBML definition for the export.
Numbers
All numbers are exported using the format pattern ###0.###
. Values for columns defined as type integer
do not contain a decimal separator or fractional digits.
Timestamps
All timestamps are exported as ISO 8601 timestamps with complete date plus hours, minutes, and seconds (but no fractional seconds) and using UTC timezone. That is, the format should match YYYY-MM-DDThh:mm:ssZ
, e.g., 2000-05-24T13:15:30Z
.
File contents
meta.json
The file meta.json
contains the JSON structure StockTaking
that can be acquired via the regular GET
call in the API when querying a stock-taking.
For enforePOS1 stock-takings, this is the same as the response from GET /org/{org-id}/stock-takings-enfore-pos-1/{stock-taking-id}
.
counting_areas.csv
The file counting_areas.csv
contains a list of all counting areas of the stock-taking.
For enforePOS1 stock-takings and area-less enforePOS2 stock-takings, this fill will be empty.
The DBML definition for the file is:
Table counting_areas {
id varchar [primary key, note: 'Technical ID of the counting area']
name varchar [not null, note: 'Name of the counting area']
type area_type [not null, note: 'Type of the counting area']
Note: 'List of the counting areas of the stock-taking'
}
Enum area_type {
STORAGE_AREA
TEMPORARY_AREA
}
participants.csv
The file participants.csv
contains a list of all participants of the stock-taking.
For enforePOS1 stock-takings, there is no device information and usually only a single participant exists.
For enforePOS2 stock-takings, there will be device information for all participants that have actually counted something. Participants that do not have counted may or may not have device information.
The DBML definition for the file is:
Table participants {
id varchar [primary key, note: 'ID of the participant']
staff_member_id varchar [not null, note: 'ID of the IndividualContact of the participant']
staff_member_name varchar [not null, note: 'Name of the participant']
device_id varchar [note: 'ID of the Device object used by the participant']
device_name varchar [note: 'Name of the Device object used by the participant']
Note: 'List of the participants of the stock-taking'
}
The value of the staff_member_id
field is the identifier of the staff member that is participant of the stock-takings. The identifier can be used to fetch the staff member via the "Staff & Access Rights"-API endpoint /org/{org-id}/staff-members/{staff-member-id}
. The identifier can also be used to fetch the IndividualContact
of the staff member via the Contacts-API endpoint /org/{org-id}/individual/{contact-id}
.
The value of the device_id
field is the identifier of the device used by the participant of the stock-taking. The identifier can be used to fetch the device via the "Organization Structure"-API endpoints /org/{org-id}/devices/{device-id}
or /org/{org-id}/enforepos-devices/{enforepos-device-id}
.
resources.csv
The file resources.csv
contains a list of all counted resource/condition pairs and aggregated count results for each.
The DBML definition for the file is:
Table resources {
id varchar
condition varchar
name varchar
article_id varchar
tracking_unit unit_of_measure [not null]
counted_units integer
first_counted_on timestamp
first_counted_by varchar
last_counted_on timestamp
last_counted_by varchar
indexes {
(id, condition) [pk]
}
Note: 'Information about the resources counted as part of the stock-taking'
}
Ref: resources.first_counted_by > participants.id
Ref: resources.last_counted_by > participants.id
Enum unit_of_measure {
ENERGY_JOULES
ENERGY_GIGAJOULES
ENERGY_MEGAJOULES
ENERGY_KILOJOULES
ENERGY_CENTIJOULES
ENERGY_MILLIJOULES
ENERGY_MICROJOULES
ENERGY_CALORIES
ENERGY_KILOCALORIES
ENERGY_WATTHOURS
ENERGY_GIGAWATTHOURS
ENERGY_MEGAWATTHOURS
ENERGY_KILOWATTHOURS
ENERGY_CENTIWATTHOURS
ENERGY_MILLIWATTHOURS
ENERGY_MICROWATTHOURS
LENGTH_KILOMETERS
LENGTH_METERS
LENGTH_DECIMETERS
LENGTH_CENTIMETERS
LENGTH_MILLIMETERS
LENGTH_POINTS
LENGTH_INCHES
LENGTH_FEET
LENGTH_YARDS
MASS_TONS
MASS_KILOGRAMS
MASS_GRAMS
MASS_MILLIGRAMS
MASS_POUNDS
QUANTITY_PIECES
TIME_SECONDS
TIME_MINUTES
TIME_HOURS
TIME_DAYS
TIME_WEEKS
DIGITALINFORMATION_BYTES
DIGITALINFORMATION_KILOBYTES
DIGITALINFORMATION_KIBIBYTES
DIGITALINFORMATION_MEGABYTES
DIGITALINFORMATION_MEBIBYTES
DIGITALINFORMATION_GIGABYTES
DIGITALINFORMATION_GIBIBYTES
DIGITALINFORMATION_TERABYTES
DIGITALINFORMATION_TEBIBYTES
DIGITALINFORMATION_PETABYTES
DIGITALINFORMATION_PEBIBYTES
TIME_MONTHS
TIME_YEARS
VOLUME_CUBIC_MILLIMETERS
VOLUME_CUBIC_CENTIMETERS
VOLUME_CUBIC_DECIMETERS
VOLUME_CUBIC_METERS
VOLUME_MILLILITERS
VOLUME_CENTILITERS
VOLUME_DECILITERS
VOLUME_LITERS
VOLUME_HECTOLITERS
VOLUME_FLUID_OUNCES
VOLUME_PINTS
VOLUME_QUARTS
VOLUME_GALLONS
VOLUME_BARRELS
}
Note that the file uses a composite primary key consisting of the resource
and condition
columns. That means, that there may be multiple records for the same resource, when the resource exists in multiple conditions.
The value of the id
field is the identifier of the resource that has been counted as part of the stock-taking. If the resource is a product, the identifier can be used to fetch the product via the ERP-API endpoint /org/{org-id}/products/{product-id}
.
The value of the condition
field identifiers the condition of the counted resource. For enforePOS1 stock-takings, the value of the column is the name of the condition (one of NEW
, REFURBISHED
, USED_LIKE_NEW
, USED_VERY_GOOD
, USED_GOOD
, USED_ACCEPTABLE
, DAMAGED
).
area_counts.csv
The file area_counts.csv
contains a list of all area counts performed as part of the stock-taking.
This file will be empty for enforePOS1 stock-taking and area-less enforePOS2 stock-takings.
The DBML definition for the file is:
Table area_counts {
id varchar [primary key]
area varchar
participant varchar
Note: 'List of the are counts of the stock-taking'
}
Ref: area_counts.area > counting_areas.id
Ref: area_counts.participant > participants.id
counting_data.csv
The file counting_data.csv
contains the low-level stock count data provided by the participants of the stock-taking.
For enforePOS1 stock-takings, low-level data is not stored, so the file essentially contains the same count data as resources.csv
.
For enforePOS2 stock-takings, low-level data is stored and made available via this file in the export.
The DBML definition for the file is:
Table counting_data {
id varchar [primary key]
resource varchar [not null]
condition varchar
lot varchar
counted_units integer [not null]
counted_on timestamp [not null]
counted_by varchar [not null]
area_count varchar
Note: 'List of counting data of the stock-taking'
}
Ref: counting_data.resource > resources.id
Ref: counting_data.counted_by > participants.id
Ref: counting_data.area_count > area_counts.id
The value of the resource
field is the identifier of the resource that has been counted as part of the stock-taking. If the resource is a product, the identifier can be used to fetch the product via the ERP-API endpoint /org/{org-id}/products/{product-id}
.
The value of the condition
field identifiers the condition of the counted resource. For enforePOS1 stock-takings, the value of the column is the name of the condition (one of NEW
, REFURBISHED
, USED_LIKE_NEW
, USED_VERY_GOOD
, USED_GOOD
, USED_ACCEPTABLE
, DAMAGED
).
The value of the lot
field is the identifier of the lot that the counted units belong to. The identifier can be used to fetch the lot via the ERP-API endpoint /org/{org-id}/lots/{lot-id}
.
counted_unique_items.csv
The file counted_unique_items.csv
contains the list of all unique item units that have been counted as part of the stock-takings.
As enforePOS1 stock-takings do not support counting unique item units, this file will be empty in exports of enforePOS1 stock-takings.
The DBML definition for the file is:
Table counted_unique_items {
id varchar [primary key]
resource varchar [not null]
lot varchar
condition varchar
counted_via varchar [not null]
Note: 'List of unique items counted as part of the stock-taking'
}
Ref: counted_unique_items.resource > resources.id
Ref: counted_unique_items.counted_via > counting_data.id
GoodsIn - Examples
Received values
Simple product
During review of the received goods, the user will set the received values. This will not only update the received_…
fields but also add entries to the received_values_change_log
.
The following JSON shows an example for a GoodsInItem
for a basic product (no custom unit, lots, conditions, unique item identifiers) where the received values have been set exactly once:
{
"id": "635f9c9f5fc3a61ae8df7861",
"product_id": "635f9ca66496bb9e6bb94f44",
"unit": {
"value": 1,
"unit": "QUANTITY_PIECES"
},
"expected_number_of_units": 10,
"received_number_of_units": 10,
"received_values_change_log": [
{
"id": "635f9ce3d3fef5e94a928e2f",
"type": "SET_RECEIVED_NUMBER_OF_UNITS",
"details": {
"@type": "SetReceivedNumberOfUnitsChangeDetail",
"new_received_number_of_units": 10,
"unit": {
"value": 1,
"unit": "QUANTITY_PIECES"
},
"delta_to_previous_quantity": {
"number_of_delta_units": 10,
"delta_unit": {
"value": 1,
"unit": "QUANTITY_PIECES"
}
},
"delta_to_expected_quantity": {
"number_of_delta_units": 0,
"delta_unit": {
"value": 1,
"unit": "QUANTITY_PIECES"
}
}
},
"timestamp": "2019-08-24T14:15:22Z"
}
]
}
Product with condition and lot
If the product uses conditions or lots, the user must also confirm/set the received_condition_id
and received_lot_id
fields during review of the received goods. Setting those values will cause additional entries to show up in the received_values_change_log
.
The following JSON shows an example for a GoodsInItem
for a product using conditions and lots where the user first confirmed condition & lot and then the received quantity:
{
"id": "635f9c9f5fc3a61ae8df7861",
"product_id": "635f9ca66496bb9e6bb94f44",
"unit": {
"value": 1,
"unit": "QUANTITY_PIECES"
},
"expected_number_of_units": 10,
"received_number_of_units": 10,
"received_values_change_log": [
// Setting received condition
{
"id": "635f9e9a3012cc3fb567d0c0",
"type": "SET_RECEIVED_CONDITION",
"details": {
"@type": "SetReceivedConditionChangeDetail",
"new_received_condition_id": "635f9e6a6e681b0aa44de228"
},
"timestamp": "2019-08-24T14:12:32Z"
},
// Setting received lot
{
"id": "635f9ea157e6f172f9896b7d",
"type": "SET_RECEIVED_LOT",
"details": {
"@type": "SetReceivedLotChangeDetail",
"new_received_lot_id": "635f9e91054ea41382105f72"
},
"timestamp": "2019-08-24T14:13:56Z"
},
// Setting received quantity
{
"id": "635f9ea8976541f10f843631",
"type": "SET_RECEIVED_NUMBER_OF_UNITS",
"details": {
"@type": "SetReceivedNumberOfUnitsChangeDetail",
"new_received_number_of_units": 10,
"unit": {
"value": 1,
"unit": "QUANTITY_PIECES"
},
"delta_to_previous_quantity": {
"number_of_delta_units": 10,
"delta_unit": {
"value": 1,
"unit": "QUANTITY_PIECES"
}
},
"delta_to_expected_quantity": {
"number_of_delta_units": 0,
"delta_unit": {
"value": 1,
"unit": "QUANTITY_PIECES"
}
}
},
"timestamp": "2019-08-24T14:15:22Z"
}
]
}
Multiple records and under/overdelivery
Sometimes the user needs to modify received values after having set the initially. For example, the user might record a received quantity less than the expected one but later discover more units of the product in the received goods.
In such cases, multiple SET_RECEIVED_…
entries for the same information (number of units, condition, lot) will be present in the log.
The following JSON shows an example for a GoodsInItem
where the user first recorded an underdelivery of two units but later adjusted the recieved number of units to an overdelivery of one unit:
{
"id": "635f9c9f5fc3a61ae8df7861",
"product_id": "635f9ca66496bb9e6bb94f44",
"unit": {
"value": 1,
"unit": "QUANTITY_PIECES"
},
"expected_number_of_units": 10,
"received_number_of_units": 11,
"received_values_change_log": [
// Initial "set received number of units" - underdelivery
{
"id": "635f9ce3d3fef5e94a928e2e",
"type": "SET_RECEIVED_NUMBER_OF_UNITS",
"details": {
"@type": "SetReceivedNumberOfUnitsChangeDetail",
"new_received_number_of_units": 8,
"unit": {
"value": 1,
"unit": "QUANTITY_PIECES"
},
"delta_to_previous_quantity": {
"number_of_delta_units": 8,
"delta_unit": {
"value": 1,
"unit": "QUANTITY_PIECES"
}
},
"delta_to_expected_quantity": {
"number_of_delta_units": -2,
"delta_unit": {
"value": 1,
"unit": "QUANTITY_PIECES"
}
}
},
"timestamp": "2019-08-24T14:15:22Z"
},
// Second "set received number of units" - overdelivery
{
"id": "635fa1e7836870497c5f12bd",
"type": "SET_RECEIVED_NUMBER_OF_UNITS",
"details": {
"@type": "SetReceivedNumberOfUnitsChangeDetail",
"new_received_number_of_units": 11,
"unit": {
"value": 1,
"unit": "QUANTITY_PIECES"
},
"delta_to_previous_quantity": {
"number_of_delta_units": 3,
"delta_unit": {
"value": 1,
"unit": "QUANTITY_PIECES"
}
},
"delta_to_expected_quantity": {
"number_of_delta_units": 1,
"delta_unit": {
"value": 1,
"unit": "QUANTITY_PIECES"
}
}
},
"timestamp": "2019-08-24T14:15:22Z"
}
]
}
Setting and clearing
In case the received values have been set by mistake, they must be cleared. For condition and lot, this is done by simply recording a SET_RECEIVED_CONDITION
/SET_RECEIVED_LOT
entry with no condition/lot identifier. For the received number of units though, a dedicated CLEAR_RECEIVED_NUMBER_OF_UNITS
entry type exists.
The reason for the dedicated entry type is that there is a difference in having received_number_of_units == 0
and received_number_of_units == null
. The former indicates that the review of the GoodsInItem
has been performed and that no units have been delivered for the item. The latter indicates that the review for the item has not yet been done.
The following JSON shows an example for a GoodsInItem
where the user first sets the received values (condition, lot, number of units) and then later clears them again:
{
"id": "635f9c9f5fc3a61ae8df7861",
"product_id": "635f9ca66496bb9e6bb94f44",
"unit": {
"value": 1,
"unit": "QUANTITY_PIECES"
},
"expected_number_of_units": 10,
"received_number_of_units": null,
"received_values_change_log": [
// Setting received condition
{
"id": "635f9e9a3012cc3fb567d0c0",
"type": "SET_RECEIVED_CONDITION",
"details": {
"@type": "SetReceivedConditionChangeDetail",
"new_received_condition_id": "635f9e6a6e681b0aa44de228"
},
"timestamp": "2019-08-24T14:12:32Z"
},
// Setting received lot
{
"id": "635f9ea157e6f172f9896b7d",
"type": "SET_RECEIVED_LOT",
"details": {
"@type": "SetReceivedLotChangeDetail",
"new_received_lot_id": "635f9e91054ea41382105f72"
},
"timestamp": "2019-08-24T14:13:56Z"
},
// Setting received quantity
{
"id": "635f9ea8976541f10f843631",
"type": "SET_RECEIVED_NUMBER_OF_UNITS",
"details": {
"@type": "SetReceivedNumberOfUnitsChangeDetail",
"new_received_number_of_units": 10,
"unit": {
"value": 1,
"unit": "QUANTITY_PIECES"
},
"delta_to_previous_quantity": {
"number_of_delta_units": 10,
"delta_unit": {
"value": 1,
"unit": "QUANTITY_PIECES"
}
},
"delta_to_expected_quantity": {
"number_of_delta_units": 0,
"delta_unit": {
"value": 1,
"unit": "QUANTITY_PIECES"
}
}
},
"timestamp": "2019-08-24T14:15:22Z"
},
// Clearing received condition
{
"id": "635fa5843b20830e34b025b5",
"type": "SET_RECEIVED_CONDITION",
"details": {
"@type": "SetReceivedConditionChangeDetail",
"new_received_condition_id": null
},
"timestamp": "2019-08-24T15:20:33Z"
},
// Clearing received lot
{
"id": "635fa58cf1c5f1e6d254d254",
"type": "SET_RECEIVED_LOT",
"details": {
"@type": "SetReceivedLotChangeDetail",
"new_received_lot_id": null
},
"timestamp": "2019-08-24T15:20:33Z"
},
// Clearing received quantity
{
"id": "635fa59101e4eba19042d2c3",
"type": "CLEAR_RECEIVED_NUMBER_OF_UNITS",
"details": {
"@type": "ClearReceivedNumberOfUnitsChangeDetail",
"delta_to_previous_quantity": {
"number_of_delta_units": -10,
"delta_unit": {
"value": 1,
"unit": "QUANTITY_PIECES"
}
},
"delta_to_expected_quantity": {
"number_of_delta_units": -10,
"delta_unit": {
"value": 1,
"unit": "QUANTITY_PIECES"
}
}
},
"timestamp": "2019-08-24T15:20:33Z"
}
]
}
Delta computation edge cases
Note that there are a number of edge cases in the context of the delta computations. Notably computing deltas against non-existing values and computing deltas for goods-in items whose unit or expected number of units are changed.
Deltas against non-existing values
Deltas against non-existing values are needed for four cases:
- for goods-in items that do not have an
expected_number_of_units
set - for all "received values change log"-entry with a
ClearReceivedNumberOfUnitsChangeDetail
- for all "received values change log"-entry with a
SetReceivedNumberOfUnitsChangeDetail
that- are the first such entry in their change log
- are the first such entry following an entry with a
ClearReceivedNumberOfUnitsChangeDetail
Often, only a single value is missing. For example for the "delta to previous quantity" for the first SetReceivedNumberOfUnitsChangeDetail
entry in the log. But there are cases where both values are missing. For example for the "delta to expected quantity" for a ClearReceivedNumberOfUnitsChangeDetail
entry.
The missing values for such delta computations are substituted by generating a quantity with the numeric value of zero and the unit/custom unit taken from the other value (if it exists) or the goods-in item (for deltas where both values are missing).
For example, conider the following goods-in item:
D1P
(marked in blue) is a "delta to previous quantity" where there is no "old received quantity" as the delta belongs to the first SetReceivedNumberOfUnitsChangeDetail
in the log. The value 0x 1pc
is substituted by taking the 1pc
unit from the "new received quantity" (from the log entry the delta belongs to) and using zero as number of units.
D2P
(marked in khaki/yellow) is a "delta to previous quantity" where there is no "new received quantity" as it belongs to a ClearReceivedNumberOfUnitsChangeDetail
entry. The value 0x 1pc
is substituted by taking the 1pc
unit from the "previous received quantity" (from the preceeding log entry) and using zero as number of units.
D3P
(marked in green) is a "delta to previous quantity" where there is no "old received quantity" as the delta belongs to the first SetReceivedNumberOfUnitsChangeDetail
log entry after a ClearReceivedNumberOfUnitsChangeDetail
entry. The value 0x 1pc
is substituted by taking the 1pc
unit from the "new received quantity" (from the log entry the delta belongs to) and using zero as number of units.
D1E
and D3E
(marked in tan/brown) are "deltas to expected quantity" where there is no "expected quantity" as the goods-in item does not have an expected number of units. The value 0x 1pc
is substituted by taking the 1pcs
unit from the "current received quantity" (from the log entries the deltas belong to) and using zero as number of units.
D2E
(marked in gold) is a "delta to expected quantity" where both the "expected quantity" and the "current received quantity" are missing. The value 0x 1pc
is substituted for both by taking the 1pcs
unit form the goods-in item and using zero as number of units.
Deltas when changing the item's unit or expected number of units
Deltas for goods-in items whose unit or expected number of units is changed require special attention as they a) are often deltas between two values that use different units and b) are adjusted based on the changed unit.
Deltas between values using different units (e.g., a delta between 2kg (received qty) and 5000g (expected qty)) are computed by first converting both values into the product's "tracking unit" (e.g., 2kg to 5000g = 2000000mg to 5000000mg = -3000000mg
, assuming mg
as tracking unit).
As deltas
For example consider the following goods-in item and assume the product uses mg
as tracking unit:
Note how a change in the item's expected number of units causes the "deltas to expected" to update:
A change to the goods-in item's unit causes most deltas to be adjusted to the changed unit too:
Note how the "deltas to the expected quantity" switched to the resource's tracking unit in case the "current received quantity" exists and uses a different unit (D1E
and D3E
) or to the item's unit in case no "curret received quantity" exists (D2E
).
New entries in the log file will use the item's new unit, so their "delta to expected" will use that one too. The "delta to previous" will usually fall back to the tracking unit as the "old received quantity" uses the old unit:
Resolutions
Basic Collect
The most commonly used type of resolution is Collect
. A Collect
indicates that the delivered goods have been taken over moved into the recipients storage system.
The following JSON shows an example for a GoodsInItem
where the full received quantity has been collected:
{
"id": "635f9c9f5fc3a61ae8df7861",
"product_id": "635f9ca66496bb9e6bb94f44",
"unit": {
"value": 1,
"unit": "QUANTITY_PIECES"
},
"expected_number_of_units": 10,
"received_number_of_units": 10,
"resolved_number_of_units": 10,
"resolutions": [
{
"id": "635fab5a1247c8cd74c0c48a",
"affected_stock": {
"number_of_units": 10,
"unit": {
"value": 1,
"unit": "QUANTITY_PIECES"
}
},
"details": {
"@type": "GoodsInItemCollectResolutionDetails"
},
"status": "BOOKED",
"status_log": [
{
"status": "PLANNED",
"timestamp": "2019-08-24T14:15:22Z"
},
{
"status": "BOOKED",
"timestamp": "2019-08-24T16:32:56Z"
}
]
}
]
}
Note that the example omits the received_values_change_log
for brevity.
Discard before Collect
Another common case is that some of the delivery goods is damaged/broken and thus cannot be collected.
If this is detected before recording the Collect
, simply recording a Discard
resolution for the broken units and then a Collect
for the remaining units is possible.
The following JSON shows an example for a GoodsInItem
where the two units are resolved as Discard
and then the remaining eight units are collected:
{
"id": "635f9c9f5fc3a61ae8df7861",
"product_id": "635f9ca66496bb9e6bb94f44",
"unit": {
"value": 1,
"unit": "QUANTITY_PIECES"
},
"expected_number_of_units": 10,
"received_number_of_units": 10,
"resolved_number_of_units": 10,
"resolutions": [
{
"id": "635fab5a1247c8cd74c0c48a",
"affected_stock": {
"number_of_units": 2,
"unit": {
"value": 1,
"unit": "QUANTITY_PIECES"
}
},
"reason": {
"@type": "PlatformDefinedGoodsInExceptionalResolutionReason",
"name": "STATE_OF_GOODS"
},
"details": {
"@type": "GoodsInItemDiscardResolutionDetails"
},
"status": "BOOKED",
"status_log": [
{
"status": "PLANNED",
"timestamp": "2019-08-24T14:15:22Z"
},
{
"status": "BOOKED",
"timestamp": "2019-08-24T14:23:33Z"
}
]
},
{
"id": "635faddf6c08f1086698fdd0",
"affected_stock": {
"number_of_units": 8,
"unit": {
"value": 1,
"unit": "QUANTITY_PIECES"
}
},
"details": {
"@type": "GoodsInItemCollectResolutionDetails"
},
"status": "BOOKED",
"status_log": [
{
"status": "PLANNED",
"timestamp": "2019-08-24T16:25:43Z"
},
{
"status": "BOOKED",
"timestamp": "2019-08-24T16:32:56Z"
}
]
}
]
}
Collect, then adjust and Discard
Sometimes, mistakes are made or stock that was seen as "good" and thus has been collected is later found to be "not good".
Any case that requires changing the outcome of an already booked resolution requires an "adjustment" of the resolution.
The following JSON shows an example for a GoodsInItem
where all ten units were Collected
but later two units were found to be defective/broken and needed to be Discarded
:
{
"id": "635f9c9f5fc3a61ae8df7861",
"product_id": "635f9ca66496bb9e6bb94f44",
"unit": {
"value": 1,
"unit": "QUANTITY_PIECES"
},
"expected_number_of_units": 10,
"received_number_of_units": 10,
"resolved_number_of_units": 10,
"resolutions": [
{
"id": "635fb36b8db8aa3a2625aca1",
"affected_stock": {
"number_of_units": 10,
"unit": {
"value": 1,
"unit": "QUANTITY_PIECES"
}
},
"details": {
"@type": "GoodsInItemCollectResolutionDetails"
},
"status": "BOOKED",
"status_log": [
{
"status": "PLANNED",
"timestamp": "2019-08-24T14:29:00Z"
},
{
"status": "BOOKED",
"timestamp": "2019-08-24T14:30:00Z"
}
],
"adjustments": [
{
"id": "635fec1267743cd6f905a40d",
"type": "DECREASE",
"affected_stock": {
"number_of_units": 2,
"unit": {
"value": 1,
"unit": "QUANTITY_PIECES"
}
},
"due_to": {
"item_id": "635f9c9f5fc3a61ae8df7861",
"resolution_id": "635fb3772ab68c151f3ce1a1"
},
"status": "BOOKED",
"status_log": [
{
"status": "PLANNED",
"timestamp": "2019-08-24T15:00:00Z"
},
{
"status": "BOOKED",
"timestamp": "2019-08-24T15:00:10Z"
}
]
}
]
},
{
"id": "635fb3772ab68c151f3ce1a1",
"affected_stock": {
"number_of_units": 2,
"unit": {
"value": 1,
"unit": "QUANTITY_PIECES"
}
},
"reason": {
"@type": "PlatformDefinedGoodsInExceptionalResolutionReason",
"name": "STATE_OF_GOODS"
},
"details": {
"@type": "GoodsInItemDiscardResolutionDetails"
},
"status": "BOOKED",
"status_log": [
{
"status": "PLANNED",
"timestamp": "2019-08-24T15:00:00Z"
},
{
"status": "BOOKED",
"timestamp": "2019-08-24T15:00:10Z"
}
]
}
]
}
Full use cases
Case 1 - overdelivery, discard, overdelivery, collect
The following JSON payload shows a GoodsInItem
with an expected quantity of 2 KOL
(where one KOL
consists of 6 pcs) after the following operations have been performed by the user:
- Setting received quantity to
3 KOL
- Booking a
Discard
of2 KOL
due to damaged goods - Booking another
Discard
of1 KOL
due to damaged goods - Updating the received quantity to
5 KOL
- Booking a
Collect
of2 KOL
{
"id": "635f9c9f5fc3a61ae8df7861",
"product_id": "635f9ca66496bb9e6bb94f44",
"unit": {
"value": 6,
"unit": "QUANTITY_PIECES"
},
"custom_unit_id": "635fb71a67743cd6f905a124",
"expected_number_of_units": 2,
"received_number_of_units": 5,
"resolved_number_of_units": 5,
"received_values_change_log": [
// 1 - Setting received quantity to `3 KOL`
{
"id": "635fb73267743cd6f905a131",
"type": "SET_RECEIVED_NUMBER_OF_UNITS",
"details": {
"@type": "SetReceivedNumberOfUnitsChangeDetail",
"new_received_number_of_units": 3,
"unit": {
"value": 6,
"unit": "QUANTITY_PIECES"
},
"custom_unit_id": "635fb71a67743cd6f905a124",
"delta_to_previous_quantity": {
"number_of_delta_units": 3,
"delta_unit": {
"value": 6,
"unit": "QUANTITY_PIECES"
},
"custom_unit_id": "635fb71a67743cd6f905a124"
},
"delta_to_expected_quantity": {
"number_of_delta_units": 1,
"delta_unit": {
"value": 6,
"unit": "QUANTITY_PIECES"
},
"custom_unit_id": "635fb71a67743cd6f905a124"
}
},
"timestamp": "2019-08-24T14:10:00Z"
},
// 4 - Updating the received quantity to `5 KOL`
{
"id": "635fb73267743cd6f905a134",
"type": "SET_RECEIVED_NUMBER_OF_UNITS",
"details": {
"@type": "SetReceivedNumberOfUnitsChangeDetail",
"new_received_number_of_units": 5,
"unit": {
"value": 6,
"unit": "QUANTITY_PIECES"
},
"custom_unit_id": "635fb71a67743cd6f905a124",
"delta_to_previous_quantity": {
"number_of_delta_units": 2,
"delta_unit": {
"value": 6,
"unit": "QUANTITY_PIECES"
},
"custom_unit_id": "635fb71a67743cd6f905a124"
},
"delta_to_expected_quantity": {
"number_of_delta_units": 3,
"delta_unit": {
"value": 6,
"unit": "QUANTITY_PIECES"
},
"custom_unit_id": "635fb71a67743cd6f905a124"
}
},
"timestamp": "2019-08-24T14:40:00Z"
}
],
"resolutions": [
// 2 - Booking a `Discard` of `2 KOL` due to damaged goods
{
"id": "635fb73267743cd6f905a132",
"affected_stock": {
"number_of_units": 2,
"unit": {
"value": 6,
"unit": "QUANTITY_PIECES"
},
"custom_unit_id": "635fb71a67743cd6f905a124"
},
"reason": {
"@type": "PlatformDefinedGoodsInExceptionalResolutionReason",
"name": "STATE_OF_GOODS"
},
"details": {
"@type": "GoodsInItemDiscardResolutionDetails"
},
"status": "BOOKED",
"status_log": [
{
"status": "PLANNED",
"timestamp": "2019-08-24T14:20:00Z"
},
{
"status": "BOOKED",
"timestamp": "2019-08-24T14:20:00Z"
}
]
},
// 3 - Booking another `Discard` of `1 KOL` due to damaged goods
{
"id": "635fb73267743cd6f905a133",
"affected_stock": {
"number_of_units": 1,
"unit": {
"value": 6,
"unit": "QUANTITY_PIECES"
},
"custom_unit_id": "635fb71a67743cd6f905a124"
},
"reason": {
"@type": "PlatformDefinedGoodsInExceptionalResolutionReason",
"name": "STATE_OF_GOODS"
},
"details": {
"@type": "GoodsInItemDiscardResolutionDetails"
},
"status": "BOOKED",
"status_log": [
{
"status": "PLANNED",
"timestamp": "2019-08-24T14:30:00Z"
},
{
"status": "BOOKED",
"timestamp": "2019-08-24T14:30:00Z"
}
]
},
// 5 - Booking a `Collect` of `2 KOL`
{
"id": "635fb73267743cd6f905a135",
"affected_stock": {
"number_of_units": 2,
"unit": {
"value": 6,
"unit": "QUANTITY_PIECES"
},
"custom_unit_id": "635fb71a67743cd6f905a124"
},
"details": {
"@type": "GoodsInItemCollectResolutionDetails"
},
"status": "BOOKED",
"status_log": [
{
"status": "PLANNED",
"timestamp": "2019-08-24T14:50:00Z"
},
{
"status": "BOOKED",
"timestamp": "2019-08-24T14:50:00Z"
}
]
}
]
}
Case 2 - overdelivery, collect, discard, overdelivery, collect
This use case is similar to use case 1, except that the initially recorded recevied quantity of 3 KOL
is immediately collected. The later Discard
resolutions then can only be performed after adjusting the initial Collect
.
The following operations are performed by the user:
- Setting received quantity to
3 KOL
- Booking a
Collect
of3 KOL
- Booking a
Discard
of2 KOL
together with a-2 KOL
adjustment of theCollect
- Booking another
Discard
of1 KOL
together with a-1 KOL
adjustment of theCollect
- Updating the received quantity to
5 KOL
- Booking a
Collect
of2 KOL
{
"id": "635f9c9f5fc3a61ae8df7861",
"product_id": "635f9ca66496bb9e6bb94f44",
"unit": {
"value": 6,
"unit": "QUANTITY_PIECES"
},
"custom_unit_id": "635fb71a67743cd6f905a124",
"expected_number_of_units": 2,
"received_number_of_units": 5,
"resolved_number_of_units": 5,
"received_values_change_log": [
// 1 - Setting received quantity to `3 KOL`
{
"id": "635fb73267743cd6f905a131",
"type": "SET_RECEIVED_NUMBER_OF_UNITS",
"details": {
"@type": "SetReceivedNumberOfUnitsChangeDetail",
"new_received_number_of_units": 3,
"unit": {
"value": 6,
"unit": "QUANTITY_PIECES"
},
"custom_unit_id": "635fb71a67743cd6f905a124",
"delta_to_previous_quantity": {
"number_of_delta_units": 3,
"delta_unit": {
"value": 6,
"unit": "QUANTITY_PIECES"
},
"custom_unit_id": "635fb71a67743cd6f905a124"
},
"delta_to_expected_quantity": {
"number_of_delta_units": 1,
"delta_unit": {
"value": 6,
"unit": "QUANTITY_PIECES"
},
"custom_unit_id": "635fb71a67743cd6f905a124"
}
},
"timestamp": "2019-08-24T14:10:00Z"
},
// 5 - Updating the received quantity to `5 KOL`
{
"id": "635fb73267743cd6f905a135",
"type": "SET_RECEIVED_NUMBER_OF_UNITS",
"details": {
"@type": "SetReceivedNumberOfUnitsChangeDetail",
"new_received_number_of_units": 5,
"unit": {
"value": 6,
"unit": "QUANTITY_PIECES"
},
"custom_unit_id": "635fb71a67743cd6f905a124",
"delta_to_previous_quantity": {
"number_of_delta_units": 2,
"delta_unit": {
"value": 6,
"unit": "QUANTITY_PIECES"
},
"custom_unit_id": "635fb71a67743cd6f905a124"
},
"delta_to_expected_quantity": {
"number_of_delta_units": 3,
"delta_unit": {
"value": 6,
"unit": "QUANTITY_PIECES"
},
"custom_unit_id": "635fb71a67743cd6f905a124"
}
},
"timestamp": "2019-08-24T14:50:00Z"
}
],
"resolutions": [
// 2 - Booking a `Collect` of `3 KOL`
{
"id": "635fb73267743cd6f905a132",
"affected_stock": {
"number_of_units": 3,
"unit": {
"value": 6,
"unit": "QUANTITY_PIECES"
},
"custom_unit_id": "635fb71a67743cd6f905a124"
},
"details": {
"@type": "GoodsInItemCollectResolutionDetails"
},
"status": "BOOKED",
"status_log": [
{
"status": "PLANNED",
"timestamp": "2019-08-24T14:20:00Z"
},
{
"status": "BOOKED",
"timestamp": "2019-08-24T14:20:00Z"
}
],
"adjustments": [
// 3b - `-2 KOL` adjustment of the `Collect` due to `Discard` of `2 KOL`
{
"id": "635fb73267743cd6f905a133b",
"type": "DECREASE",
"affected_stock": {
"number_of_units": 2,
"unit": {
"value": 6,
"unit": "QUANTITY_PIECES"
},
"custom_unit_id": "635fb71a67743cd6f905a124"
},
"due_to": {
"item_id": "635f9c9f5fc3a61ae8df7861",
"resolution_id": "635fb73267743cd6f905a133"
},
"status": "BOOKED",
"status_log": [
{
"status": "PLANNED",
"timestamp": "2019-08-24T14:30:00Z"
},
{
"status": "BOOKED",
"timestamp": "2019-08-24T14:30:00Z"
}
]
},
// 4b - `-1 KOL` adjustment of the `Collect` due to `Discard` of `1 KOL`
{
"id": "635fb73267743cd6f905a134b",
"type": "DECREASE",
"affected_stock": {
"number_of_units": 1,
"unit": {
"value": 6,
"unit": "QUANTITY_PIECES"
},
"custom_unit_id": "635fb71a67743cd6f905a124"
},
"due_to": {
"item_id": "635f9c9f5fc3a61ae8df7861",
"resolution_id": "635fb73267743cd6f905a134"
},
"status": "BOOKED",
"status_log": [
{
"status": "PLANNED",
"timestamp": "2019-08-24T14:40:00Z"
},
{
"status": "BOOKED",
"timestamp": "2019-08-24T14:40:00Z"
}
]
}
]
},
// 3a - Booking a `Discard` of `2 KOL`
{
"id": "635fb73267743cd6f905a133",
"affected_stock": {
"number_of_units": 2,
"unit": {
"value": 6,
"unit": "QUANTITY_PIECES"
},
"custom_unit_id": "635fb71a67743cd6f905a124"
},
"reason": {
"@type": "PlatformDefinedGoodsInExceptionalResolutionReason",
"name": "STATE_OF_GOODS"
},
"details": {
"@type": "GoodsInItemDiscardResolutionDetails"
},
"status": "BOOKED",
"status_log": [
{
"status": "PLANNED",
"timestamp": "2019-08-24T14:30:00Z"
},
{
"status": "BOOKED",
"timestamp": "2019-08-24T14:30:00Z"
}
]
},
// 4a - Booking another `Discard` of `1 KOL`
{
"id": "635fb73267743cd6f905a134",
"affected_stock": {
"number_of_units": 1,
"unit": {
"value": 6,
"unit": "QUANTITY_PIECES"
},
"custom_unit_id": "635fb71a67743cd6f905a124"
},
"reason": {
"@type": "PlatformDefinedGoodsInExceptionalResolutionReason",
"name": "STATE_OF_GOODS"
},
"details": {
"@type": "GoodsInItemDiscardResolutionDetails"
},
"status": "BOOKED",
"status_log": [
{
"status": "PLANNED",
"timestamp": "2019-08-24T14:40:00Z"
},
{
"status": "BOOKED",
"timestamp": "2019-08-24T14:40:00Z"
}
]
},
// 6 - Booking a `Collect` of `2 KOL`
{
"id": "635fb73267743cd6f905a136",
"affected_stock": {
"number_of_units": 2,
"unit": {
"value": 6,
"unit": "QUANTITY_PIECES"
},
"custom_unit_id": "635fb71a67743cd6f905a124"
},
"details": {
"@type": "GoodsInItemCollectResolutionDetails"
},
"status": "BOOKED",
"status_log": [
{
"status": "PLANNED",
"timestamp": "2019-08-24T15:00:00Z"
},
{
"status": "BOOKED",
"timestamp": "2019-08-24T15:00:00Z"
}
]
}
]
}
Case 3 - overdelivery, discard, revert discard, discard, underdelivery
The following JSON payload shows a GoodsInItem
with an expected quantity of 2 KOL
(where one KOL
consists of 6 pcs) after the following operations have been performed by the user:
- Setting received quantity to
3 KOL
- Booking a
Discard
of2 KOL
due to damaged goods - Reverting the
Discard
(that is, adjust it to effectively zero units) - Booking another
Discard
of1 KOL
due to damaged goods - Updating the received quantity to
1 KOL
{
"id": "635f9c9f5fc3a61ae8df7861",
"product_id": "635f9ca66496bb9e6bb94f44",
"unit": {
"value": 6,
"unit": "QUANTITY_PIECES"
},
"custom_unit_id": "635fb71a67743cd6f905a124",
"expected_number_of_units": 2,
"received_number_of_units": 1,
"resolved_number_of_units": 1,
"received_values_change_log": [
// 1 - Setting received quantity to `3 KOL`
{
"id": "635fb73267743cd6f905a131",
"type": "SET_RECEIVED_NUMBER_OF_UNITS",
"details": {
"@type": "SetReceivedNumberOfUnitsChangeDetail",
"new_received_number_of_units": 3,
"unit": {
"value": 6,
"unit": "QUANTITY_PIECES"
},
"custom_unit_id": "635fb71a67743cd6f905a124",
"delta_to_previous_quantity": {
"number_of_delta_units": 3,
"delta_unit": {
"value": 6,
"unit": "QUANTITY_PIECES"
},
"custom_unit_id": "635fb71a67743cd6f905a124"
},
"delta_to_expected_quantity": {
"number_of_delta_units": 1,
"delta_unit": {
"value": 6,
"unit": "QUANTITY_PIECES"
},
"custom_unit_id": "635fb71a67743cd6f905a124"
}
},
"timestamp": "2019-08-24T14:10:00Z"
},
// 5 - Updating the received quantity to `1 KOL`
{
"id": "635fb73267743cd6f905a135",
"type": "SET_RECEIVED_NUMBER_OF_UNITS",
"details": {
"@type": "SetReceivedNumberOfUnitsChangeDetail",
"new_received_number_of_units": 1,
"unit": {
"value": 6,
"unit": "QUANTITY_PIECES"
},
"custom_unit_id": "635fb71a67743cd6f905a124",
"delta_to_previous_quantity": {
"number_of_delta_units": -2,
"delta_unit": {
"value": 6,
"unit": "QUANTITY_PIECES"
},
"custom_unit_id": "635fb71a67743cd6f905a124"
},
"delta_to_expected_quantity": {
"number_of_delta_units": -1,
"delta_unit": {
"value": 6,
"unit": "QUANTITY_PIECES"
},
"custom_unit_id": "635fb71a67743cd6f905a124"
}
},
"timestamp": "2019-08-24T14:50:00Z"
}
],
"resolutions": [
// 2 - Booking a `Discard` of `2 KOL` due to damaged goods
{
"id": "635fb73267743cd6f905a132",
"affected_stock": {
"number_of_units": 2,
"unit": {
"value": 6,
"unit": "QUANTITY_PIECES"
},
"custom_unit_id": "635fb71a67743cd6f905a124"
},
"reason": {
"@type": "PlatformDefinedGoodsInExceptionalResolutionReason",
"name": "STATE_OF_GOODS"
},
"details": {
"@type": "GoodsInItemDiscardResolutionDetails"
},
"status": "BOOKED",
"status_log": [
{
"status": "PLANNED",
"timestamp": "2019-08-24T14:20:00Z"
},
{
"status": "BOOKED",
"timestamp": "2019-08-24T14:20:00Z"
}
],
"adjustments": [
// 3 - Reverting the `Discard`
{
"id": "635fb73267743cd6f905a133",
"type": "DECREASE",
"affected_stock": {
"number_of_units": 2,
"unit": {
"value": 6,
"unit": "QUANTITY_PIECES"
},
"custom_unit_id": "635fb71a67743cd6f905a124"
},
"reason": {
"@type": "PlatformDefinedGoodsInResolutionAdjustmentReason",
"name": "HUMAN_ERROR"
},
"status": "BOOKED",
"status_log": [
{
"status": "PLANNED",
"timestamp": "2019-08-24T14:30:00Z"
},
{
"status": "BOOKED",
"timestamp": "2019-08-24T14:30:00Z"
}
]
}
]
},
// 4 - Booking another `Discard` of `1 KOL` due to damaged goods
{
"id": "635fb73267743cd6f905a134",
"affected_stock": {
"number_of_units": 1,
"unit": {
"value": 6,
"unit": "QUANTITY_PIECES"
},
"custom_unit_id": "635fb71a67743cd6f905a124"
},
"reason": {
"@type": "PlatformDefinedGoodsInExceptionalResolutionReason",
"name": "STATE_OF_GOODS"
},
"details": {
"@type": "GoodsInItemDiscardResolutionDetails"
},
"status": "BOOKED",
"status_log": [
{
"status": "PLANNED",
"timestamp": "2019-08-24T14:40:00Z"
},
{
"status": "BOOKED",
"timestamp": "2019-08-24T14:40:00Z"
}
]
}
]
}
Case 4 - regular delivery, collect, reset to planned, overdelivery, collect + discard
The following JSON payloads shows a GoodsInItem
with an expected quantity of 10 pcs
after the following operations have been performed by the user:
- Set received quantity to
10 pcs
- Book a
Collect
of all10 pcs
- Reset of the goods-in to
PLANNED
- Set received quantity to
12 pcs
- Book a
Collect
for10 pcs
- Book a
Discard
for2 pcs
{
"id": "635f9c9f5fc3a61ae8df7861",
"product_id": "635f9ca66496bb9e6bb94f44",
"unit": {
"value": 1,
"unit": "QUANTITY_PIECES"
},
"expected_number_of_units": 10,
"received_number_of_units": 12,
"resolved_number_of_units": 12,
"received_values_change_log": [
// 1 - Setting received quantity to `10 pcs`
{
"id": "644a8908cd019709e4a27953",
"type": "SET_RECEIVED_NUMBER_OF_UNITS",
"details": {
"@type": "SetReceivedNumberOfUnitsChangeDetail",
"new_received_number_of_units": 10,
"unit": {
"value": 1,
"unit": "QUANTITY_PIECES"
},
"delta_to_previous_quantity": {
"number_of_delta_units": 10,
"delta_unit": {
"value": 1,
"unit": "QUANTITY_PIECES"
}
},
"delta_to_expected_quantity": {
"number_of_delta_units": 0,
"delta_unit": {
"value": 1,
"unit": "QUANTITY_PIECES"
}
}
},
"timestamp": "2019-08-24T14:10:00Z"
}
// 3 - Reset goods-in to PLANNED
{
"id": "644a8e708fdb955f3cc69cb5",
"type": "RESET_TO_PLANNED",
"details": {
"@type": "ResetToPlannedChangeDetail",
},
"timestamp": "2019-08-24T14:30:00Z"
}
// 4 - Setting received quantity to `12 pcs`
{
"id": "644a8e7c9f304ffb76e102ae",
"type": "SET_RECEIVED_NUMBER_OF_UNITS",
"details": {
"@type": "SetReceivedNumberOfUnitsChangeDetail",
"new_received_number_of_units": 12,
"unit": {
"value": 1,
"unit": "QUANTITY_PIECES"
},
"delta_to_previous_quantity": {
"number_of_delta_units": 12,
"delta_unit": {
"value": 1,
"unit": "QUANTITY_PIECES"
}
},
"delta_to_expected_quantity": {
"number_of_delta_units": 2,
"delta_unit": {
"value": 1,
"unit": "QUANTITY_PIECES"
}
}
},
"timestamp": "2019-08-24T14:40:00Z"
}
],
"resolutions": [
// 2 - Booking a `Collect` of `10 pcs`
{
"id": "644a89bddfe12d88c0433adc",
"affected_stock": {
"number_of_units": 10,
"unit": {
"value": 1,
"unit": "QUANTITY_PIECES"
}
},
"details": {
"@type": "GoodsInItemCollectResolutionDetails"
},
"status": "ANNULLED",
"status_log": [
{
"status": "PLANNED",
"timestamp": "2019-08-24T14:20:00Z"
},
{
"status": "BOOKED",
"timestamp": "2019-08-24T14:20:00Z"
},
{
// 3 - Reset goods-in to PLANNED causes annullment of previously booked resolutions
"status": "ANNULLED",
"timestamp": "2019-08-24T14:30:00Z"
}
],
"adjustments": [
// 3 - Reset goods-in to PLANNED causes annullment of previously booked resolutions
{
"id": "644a8aa84669fd2b1186e9ad",
"type": "DECREASE",
"affected_stock": {
"number_of_units": 10,
"unit": {
"value": 1,
"unit": "QUANTITY_PIECES"
}
},
"status": "BOOKED",
"status_log": [
{
"status": "PLANNED",
"timestamp": "2019-08-24T14:30:00Z"
},
{
"status": "BOOKED",
"timestamp": "2019-08-24T14:30:00Z"
}
]
}
]
}
// 5 - Booking a `Collect` of `10 pcs`
{
"id": "644a899c889e61f23f436c8b",
"affected_stock": {
"number_of_units": 10,
"unit": {
"value": 1,
"unit": "QUANTITY_PIECES"
}
},
"details": {
"@type": "GoodsInItemCollectResolutionDetails"
},
"status": "BOOKED",
"status_log": [
{
"status": "PLANNED",
"timestamp": "2019-08-24T14:50:00Z"
},
{
"status": "BOOKED",
"timestamp": "2019-08-24T14:50:00Z"
}
]
}
// 6 - Booking a `Discard` of `2 pcs`
{
"id": "644a89d991790cf2ea45b29a",
"affected_stock": {
"number_of_units": 2,
"unit": {
"value": 1,
"unit": "QUANTITY_PIECES"
}
},
"reason": {
"@type": "PlatformDefinedGoodsInExceptionalResolutionReason",
"name": "NOT_ORDERED"
},
"details": {
"@type": "GoodsInItemDiscardResolutionDetails"
},
"status": "BOOKED",
"status_log": [
{
"status": "PLANNED",
"timestamp": "2019-08-24T15:00:00Z"
},
{
"status": "BOOKED",
"timestamp": "2019-08-24T15:00:00Z"
}
]
},
]
}
GoodsOut
Cancellation requests
The cancellation of a goods-out can be desired by different actors for various reasons.
By goods-out staff (GOS)
There are various reasons why the person asked to process the goods-out may want it to get canceled. For example, there may not be enough stock to pick or packaging material. There may not be enough space or time to perform the picking and packing. There may be some power outage. Some machine needed (e.g., a label printer, a pallet jack) may be broken or otherwise not available.
As long as the GoodsOut
is still unconfirmed, the goods-out staff could simply reject it. But once it has been confirmed, that is no longer possible. And simply canceling the GoodsOut
from the "processing side" is not allowed as goods-out are "depending business processes" and thus their cancellation must be controlled via their "parent process".
From higher-level business process (HLP)
A high-level business process that is parent of one or more goods-out may want those to get canceled because it may be canceled itself or because the existing goods-out are not needed/wanted anymore. For example when a different location shall perform the goods-out or when a delivery is to be split.
As long as the GoodsOut
is not yet "in process", it could simply be canceled as the parent process has the right to do so. But when picking has already started, this would cause a problem with the picked stock. The goods-out staff has picked things and they are not "in stock" anymore. Automatically booking them back to "in stock" would not cause them to actually move "in the real world". The goods-out staff would also not really get any notification, "his" GoodsOut
would simply not be shown in the UI anymore.
Cancellation workflow/logic:
Thus, the following workflow/logic should be used when requesting and performing cancellations:
-
GOS wants to cancel
- GOS issues request describing the wish to cancel
- HLP accepts or rejects the request
- If rejected
- Workflow is finished
- If accepted and GoodsOut is still UNCONFIRMED or CONFIRMED
- GoodsOut can directly get canceled, "accept" can be sent as secondary+before to "cancel"
- If accepted and GoodsOut is already IN_PROCESS
- GOS must trigger cancellation by executing "performFullCancellation"
-
HLP wants to cancel
- HLP issues request describing the need/wish to cancel
- GOS accepts the request
- We may want to allow GOS to reject, but that would need some kind of setting or user right that allows this.
- If rejected (once we allow this)
- Workflow is finished
- If accepted and GoodsOut is still UNCONFIRMED or CONFIRMED
- GoodsOut can directly get canceled, "accept" can be sent as secondary+before to "cancel"
- If accepted and GoodsOut is already IN_PROCESS
- GoodsOut can get canceled, "accept" can be set as part of "performFullCancellation"
Life-cycle / Active
A cancellation request has a simple life cycle consisting of the states REQUESTED
, ACCEPTED
, REJECTED
and WITHDRAWN
.
Additionally, it has a cancellation_performed
flag. This flag is not a state as it does not represent a property of the request but of the goods-out.
Requests are considered to be "active" when they are in state PENDING
or ACCEPTED
and their cancellation_performed
flag is false.
Introduction (Spec)
This documentation describes the high-level architecture of the enfore Ledger-API.
The enfore Ledger-API is provided by the enfore platform to be convenient to use for ledger use cases, for example when integrating with an external ledger system.
The API consists of multiple modules that allow
- notification about and querying of accounts
- notification about and querying of booking periods
- notification about and querying of transactions
The exact set of modules and their capabilities will evolve over time, the initial API version will only support a very limited set of use cases.
Use cases
The aim of the Ledger-API is to be useful for implementing use cases related to observing ledger/accounting data recorded in the enfore platform and copying/transmission of such data to third party systems.
For now, the API only supports
- Direct, immediately executed queries against the enfore ledger backend
- Subscribing to notifications about accounts, booking periods and transactions
Domain model
The "ledger" domain model of the enfore platform consists of objects that can be separated into three groups: transactions, accounts, and booking periods.
A transaction is a record of the movement of a monetary amount from one account to another. An account is a holder of a monetary amount (aka balance) that is modified by transactions adding to or removing from that balance. Booking periods are timespans that are used to group transactions based on the need of the business rather than a general calendar (see "business day" vs. "calendar day")
Accounts
From a technical point of view, an account is simply a container for a monetary amount called balance. From an accounting point of view, accounts have meaning based on their type and additional properties.
The accounts used by an organization make up its "chart of accounts" (COA) - a financial, organizational tool that provides an index of every account in an accounting system.
Some of these accounts appear on the balance sheets of an organization. They give a summary of the financial balances of the organization, a snapshot of an organization's financial condition, so to speak.
The accounts of the balance sheet represent liabilities, assets and ownership equity of the organization. In our domain model most of them use the Type BalanceSheetAccount.
Some accounts for assets or liabilities are used for PaymentTransactions. We call them "money accounts". Currently there are two kinds of money accounts in our domain model:
- certain BalanceSheetAccounts
- CashAccounts
Other "money accounts" will be added later, e.g. BankAccounts.
Other accounts in the COA represent revenue and expenses of the organization.
Furthermore there are accounts representing business partners in their roles as a creditor or debtor, as explained below. They can be seen as sub-accounts of the "accounts receivable" and "accounts payable" accounts of the COA.
Using different account types allows to to define transactions that only work on specific types of accounts.
Balance-sheet accounts
As mentioned, most accounts on the balance sheet use the type BalanceSheetAccount. Typical examples:
- the asset account for "accounts receivable"
- the liability account for "accounts payable"
- the asset account for cash in transit
- the asset account for transit items like tip or escrow sales
- the account for the liabilities caused by voucher sales
The latter is an example for a BalanceSheetAccount that can be used as money account.
Many of the BalanceSheetAccounts are not used by any workflows in enforePOS yet, and the available accounts heavily depend on the country of the organization. In a country like Germany, where detailed standardized Charts of Accounts exist, the number of BalanceSheetAccounts can be huge. In other countries, where only coarse specifications for a Chart of Accounts exist, the number of BalanceSheetAccounts provided by the domain model will be small.
Cash accounts
Cash accounts are used to track physical money (that is, coins and bank notes). In the real world, they are therefore represented by some kind of container like a waiter's wallet or a strongbox.
The enfore platform differentiates between three types of cash accounts based on the "container type":
WALLET
- a wallet is only available as personalized cash containers belonging to a specific staff member like waiterCASH_DRAWER
- always located at a particular cash register, either assigned to that register or to a specific staff member; in the former case it is used by all staff members, in the latter case only be the assigned staff memberVAULT
- for cash containers that belong to a service location rather than a specific staff member or register and are not used used for register-level transactions (e.g. sales and refunds).PETTY_CASH
- for cash containers that belong to a service location rather than a specific staff member or register and are not used used for register-level transactions (e.g. sales and refunds).
There are some additional constraints imposed by the enfore plaform regarding management of cash containers:
- A Staff member's personalized cash container is managed as a cash account with container type
WALLET
orCASH_DRAWER
on a "per location"-basis. That means, if the staff member works at more than one location (e.g., a waiter switching between different branches of a chain), a separate cash account exists for each location and money must be explicitly transferred between those by recording an appropriate money transfer. - Staff members that are assigned a such cash account cannot perform cash-related operations on registers that are assigned a
CASH_DRAWER
-type cash account. This is to avoid the ambiguity of "where does the money go/come from" when that staff member sells/refunds something via that register. - CashAccounts of type
VAULT
can't be used to record PaymentTransactions for revenue, only MoneyTransfers and PaymentTransactions for expenses can be recorded.
Expense accounts
ExpenseAccounts track the expenses of an organization. Depending on the COA being used, there could be one general expense account or many expense accounts where each account represents a particular kind of expense (e.g. travel cost, education etc.).
Income accounts
IncomeAccounts track the revenue made by an organization. Currently they are not exposed by the domain model.
Creditor accounts
A creditor' refers to the party that has delivered a product, service or loan, and is owed money by the organization. A creditor account tracks the credit amounts and their reconciliation for a specific creditor. Creditor accounts are assigned to staff members and suppliers (contacts with a staff or supplier role).
If a creditor in a business process involving transactions is not known (like in expenses made by a supplier not tracked by the organization), many systems use the "accounts payable" account instead of a CreditorAccount. In our model an "Miscellaneous Supplier" with an assigned CreditorAccount is used. This allows to use CreditorAccounts in transactions in a type-safe way and also allows to require the associated fields in the domain model for transactions using CreditorAccounts.
Debtor accounts
A debtor is a party who owes money to the organization. A debtor account tracks the owed amounts and their reconciliation for a specific debtor. Debtor accounts are assigned to customers (contacts with a customer role) or alternative payers (contacts with an alternative payer role).
Usually a DebtorAcount is used for specific customers (or alternative payers) as the debtor.
If a debtor in a business process involving transactions is not known (like in sales to an unknown customer), many systems use the "accounts receivable" account instead of a DebtorAccount. In our model an "Anonymous Customer" with an assigned DebtorAccount is used. This allows to use DebtorAccounts in transactions in a type-safe way and also allows to require the associated fields in the domain model for transactions using DebtorAccounts.
Tracking accounts
Explained in the section about booking periods.
Transactions
Transactions move monetary amounts between accounts. Depending on the types of the accounts involved as well as the meaning of the transaction, different transaction types are used. All transaction types share the same set of basic information (such as number, amount, recording/processing timestamp and by whom/where they were recorded) but have additional field to holds the information necessary for the specific type of transaction.
Money transfers
MoneyTransfers are transactions that differ fundamentally from other transactions in two aspects:
- they always transfer money between two accounts on the balance sheet (BalanceSheetAccounts and money accounts)
- they do not have "items", so the whole transfer fulfills a single purpose
The two accounts between them money is transferred are given by the fields active_account
and target_account
. The former is the account where the transfer is recorded. If this account is a CashAccount with a TSE, this TSE is used to cover the transaction. Depending on the value of the field direction
money is either moved away from that account (direction OUTGOING) to the target_account
or moved into that account (direction INCOMING) from the target_account
.
Transfer of money can be recorded as one MoneyTransfer of type SINGLE or as a pair of MoneyTransfers with types ORIGIN and CONTINUATION that reference each other through the counterpart
field and can be recorded at different points in time. If the target_account
of a CONTINUATION type MoneyTransfer is a CashAccount with a TSE, this TSE ist used to cover the transaction.
Depending on the type of MoneyTransfers, it is possible that at least one of two accounts in the transfer is empty temporarily:
- type SINGLE: both accounts are set from the beginning
- type ORIGIN: target_account may be empty until the matching CONTINUATION is recorded
- type CONTINUATION: both accounts are set from the beginning
Expense transactions
ExpenseTransactions track expenses of the organization. They contain one or more ExpenseItems, each of them representing a single purchase of goods or services made by the organization. Each ExpenseItem references an ExpenseAccount, and the whole transactions references a CreditorAccount, representing the person or organization that needs to be paid for the received goods or services. So in total the ExpenseTransaction moves money between one or more ExpenseAccounts and a CreditorAccount. If the transactions has the type "CREDIT", the organization owes money to the creditor and the CreditorAccount is charged with a negative amount. If the type is "DEBIT", it's the other way around. This represents a reversal of a prior expense.
Currently ExpenseTransactions in enforePOS only apear as simple transactions with a single item and always directly paid by cash and using the CreditorAccount of the "Miscellaneous Supplier".
IncomeTransactions
IncomeTransactions track revenue made by the organization. In total they move money between one or more IncomeAccounts and a DebtorAccount. A typical use case for an IncomeTransaction is a sales invoice. Additionally, income transactions are created when handling cash overhangs during business day management.
For income transactions created for sales invoices, more information can be accessed via the ERP-API.
Payment transactions
PaymentTransactions represent payments for "payable" transactions (ExpenseTransactions or IncomeTransactions) and those other transactions are "paid by" the former. They always move money between a money account and a DebtorAccount or CreditorAccount.
So if we look on an ExpenseTransaction and a PaymentTransaction that pays it, together they move money between one or more ExpenseAccounts and a money account, using a CreditorAccount as intermediate account, that keeps track of the liability that exists in the time between recording the expense and recording the payment.
In the same way, an IncomeTransaction and the PaymentTransaction that pays it together move money between one or more IncomeAccounts and a money account, using a DebtorAccount as intermediate account, that keeps track of the
In general the relation between PaymentTransactions and their paid transactions is m:n, in most cases either 1:1, 1:n or m:1. To represent these relationships, PaymentTransactions have an array of "ReferencedTransactions" and ExpenseTransactions or IncomeTransactions have an array of "payments".
Both are dedicated data structures that hold the necessary information.
Payments
Payment
structures are used in transactions paid by other transactions, to record how much was paid via what transaction at what point in time. Note that a transaction can be paid for in parts, resulting in multiple Payment
structures being present in the paid transaction.
This kind of structure is used by expense transactions and by inceome transactions.
For each payment that is made for a transaction, that transaction will received a Payment
structure with:
Field | Meaning |
---|---|
transaction | The transaction that paid for the transaction holding the Payment structure |
method | The method of payment that was used |
amount | The amount that was paid |
timestamp | The point in time when the payment was processed |
Referenced transactions
ReferencedTransaction
structures are used in transactions that are payments for other transactions, to record how much of the transaction was payment for the other transaction. As a single transaction can be payment for multiple other transactions, multiple ReferencedTransaction
structures may be present in the paying transaction.
This kind of structure is used by payment transactions and by balance-sheet transactions that represent payments.
For each transaction that is fully or partially paid for by a transaction, that transaction will received a ReferencedTransaction
structure with:
Field | Meaning |
---|---|
transaction | The transaction that is paid by the transaction holding the ReferencedTransaction structure |
amount | The amount that was paid |
Example
As an example, consider a user recording a cash drawing for an expense. This will create
- an
ExpenseTransaction
that records money being moved from theCreditorAccount
of the supplier to the relevantExpenseAccount
(based on the type of expense) - a
PaymentTransaction
that records money being moved from theCashAccount
to theCreditorAccount
of the supplier
The expense transaction causes the expense's amount to be deducted from the creditor account (meaning the creditor is owned that amount) and added to the expense account (meaning an expense occurred).
The payment transaction causes the payment's amount to be deducted from the cash account (as money was taken out) and added to the creditor account (settling the liability caused by the expense transaction).
Both transactions are linked by their Payment
/ReferencedTransaction
structures to represent that fact that the payment transaction paid for the expense transaction.
Stripped down to the cross references, the expense transaction looks like this:
{
"id": "et_0001",
"creditor_account": {
"account_id": "cra_0002",
"account_type": "CREDITOR_ACCOUNT"
},
"items": [
{
"account": {
"account_id": "ea_0003",
"account_type": "EXPENSE_ACCOUNT"
}
}
],
"payments": [
{
"transaction": {
"transaction_id": "pt_0004",
"transaction_type": "PAYMENT_TRANSACTION"
}
}
]
}
And the payment transaction looks like this:
{
"id": "pt_0004",
"money_account": {
"account_id": "caa_0005",
"account_type": "CASH_ACCOUNT"
},
"payee_account": {
"account_id": "cra_0002",
"account_type": "CREDITOR_ACCOUNT"
},
"referenced_transactions": [
{
"transaction": {
"transaction_id": "et_0001",
"transaction_type": "EXPENSE_TRANSACTION"
}
}
]
}
Booking Periods
Accounts keep a balance, that reflects the total effect of all transactions made on this account. Booking periods keep track of these balances over time. They also accumulate some more KPIs that currently are not exposed through the ledger API.
Booking periods can be started and closed automatically on events, in recurring time frames or based on explicit user actions. They will always record the account balance in different states:
- when the period is created (and the balance is taken over from the closing balance of the prior period)
- when the period is started
- when the period is closed
Though basically every account could use booking periods, they are currently used only on CashAccounts and TrackingAccounts.
Booking periods of TrackingAccounts
Tracking Accounts never appear as account in transactions, they only accumulate the effect of transactions made on other accounts. They are automatically created for:
- POSLocations
- Staff members working as cashiers at a POSLocation
- Cash register devices
Every transaction made at a particular POSLocation will always reference a device and a staff member, and these are assigned to a POSLocation, so every such transaction can be assigned to the current booking period of the tracking accounts of these objects. They modify the balance of these tracking accounts and their booking periods.
In the enforePOS application the booking periods are represented by the register or staff member reports in the accounting UI. Their life cycles exactly match those of the business days or cashier shifts of the owners of the tracking accounts.
Booking periods of CashAccounts
Cash register devices and staff members working as cashiers both can own cash accounts. If they do, enforePOS automatically creates booking periods not only for their tracking accounts, but also for their cash accounts. Their life cyle is exactly in sync with the life cycle of the tracking accounts.
CashAccounts add one feature to booking periods, because they might require counting of cash money at times. The result of that counting might differ from the current balance of the booking period, that is calculated from the recorded transactions. enforePOS requires that these differences are reconciled by recording a suitable transaction before the period is closed. To keep track of that difference, the booking period provides two additional balances: the manual_end_balance
for the total amount of the final counting and the closing_difference
for the amount of the transaction that is recorded to make the current balance of the period
match the manual_end_balance
.
Introduction (Spec)
This documentation describes the high-level architecture of the enfore Organization Structure & Configuration-API.
The enfore Organization Structure & Configuration-API is provided by the enfore platform to be allow programmatic setup/modification of the structural elements of an organization as represented in the enforePOS platform.
The API consists of multiple modules that allow
- notification about, querying and creation/update of POS sales channels
- notification about, querying and creation/update of service locations
The exact set of modules and their capabilities will evolve over time, the initial API version will only support a very limited set of use cases.
Use cases
Domain model
The domain model of the enfore Organization Structure & Configuration-API consists of the core objects:
POSSalesChannel
ServiceLocation
ProcessingLocation
StorageLocation
The enfore platform uses sales channels as a concept to model different ways via wich to sell products to customer. A POSSalesChannel
is a sales channel that is realized by a physical store location. Other types of sales channels are used to represent sales via online channels such as the enfore OnlinePresence, Amazon, Ebay or a custom online store.
Every direct physical interaction with customers is performed at a ServiceLocation
. As such a service locations represents a physical place that customers can access and must have a name and an address.
There is a 1:1 relationship between POSSalesChannel
s and ServiceLocation
s. That is, there is exactly one ServiceLocation
for each POSSalesChannel
and vice versa. This combination is also called a "POS location", for example in the enforePOS client application UI.
In a POS location, the sales channel is used to configure how products are sold (e.g., is pickup possible). Whereas the service location is used to configure how the sale is recorded (e.g., cash management settings, settings for how revenue is booked) and fulfilled (e.g., where are productions, pickups and deliveries performed).
For the production of items and the preparation of pickup and delivery operations, a ProcessingLocation
is needed. These processing locations always belong to a specific service location, even though they might have a different phyiscal location (e.g., a garage where bicycles are built that are sold from a store across the street).
The ProcessingLocation
holds configuration for the production, pickup and delivery processes. For example, what type of tracking is enabled. Or which documents are printed and what printer to use.
TBD: What about inconsistencies between SalesChannel and AssemblyLocation settings. E.g., SalesChannel states "Delivery splip is printed" but AssemblyLocation does not have this setting and might not even have a printer configured?
A StorageLocation
is a place where the organization stores physical goods such as inventory items or raw materials.
Introduction (Spec)
This documentation describes the high-level architecture of the enfore Promotions-API.
The enfore Promotions-API is provided by the enfore platform to allow management of promotions and loyalty programs as well as related objects such as sales vouchers.
The API consists of multiple modules that handle
- External Loyalty Programs
- Sales Voucher Gift Cards
- Sales Voucher Product Vouchers
- Sales Voucher Category Vouchers
Introduction (Spec)
This documentation describes the high-level architecture of the enfore Reservation-API.
The enfore Reservation-API is TBD
Introduction (Spec)
This documentation describes the high-level architecture of the enfore Sales and Purchasing API.
The enfore Sales and Purchasing API is provided by the enfore platform to be convenient to use for managing sales orders in the "Retail" vertical, for example when integrating with an external system such as SAP or Dynamics.
Use cases
The aim of the Sales and Purchasing-API is to be useful for implementing use cases related to sales in the "Retail" vertical.
Initially, the API will only support a small set of use cases:
- Fetching sales order information
- Receiving notifications about newly created sales orders
- Creation of new sales orders
- Updating status of sales orders (and related entities such as productions and fulfillments)
Fetching sales order information
A main use case of the Sales and Purchasing-API is the ability to page through existing sales orders.
For paging through existing sales orders, the Sales and Purchasing-API provides the endpoint GET /org/{org-id}/sales-orders/
. That endpoint follows the normal paging-pattern of the enfore-APIs in that the client must specifiy a time-window via from
and to
and can then page within that window using offset
and limit
.
For example, the following requests can be used to access the first 200 sales orders whose transaction timestamp lies between 1pm and 3pm CEST on May 1st 2019:
GET /org/{org-id}/sales-orders/?from=2017-05-01T11%3A00%3A00Z&to=2017-05-01T13%3A00%3A00Z&limit=50&offset=0
(fetches items 0 to 49)GET /org/{org-id}/sales-orders/?from=2017-05-01T11%3A00%3A00Z&to=2017-05-01T13%3A00%3A00Z&limit=50&offset=50
(fetches items 50 to 99)GET /org/{org-id}/sales-orders/?from=2017-05-01T11%3A00%3A00Z&to=2017-05-01T13%3A00%3A00Z&limit=50&offset=100
(fetches items 100 to 149)GET /org/{org-id}/sales-orders/?from=2017-05-01T11%3A00%3A00Z&to=2017-05-01T13%3A00%3A00Z&limit=50&offset=150
(fetches items 150 to 199)
Receiving notifications about newly created sales orders
Often times, clients are interested in sales orders that are newly created (for example via the enforePOS client UI). To not have to continously poll the paging endpoint, the sales order API provides a way to register for newly created sales orders via the subscription API.
The event type for newly created sales orders is SALES_ORDERS_NEW
.
Whenever a new sales order is created, the registered callback will be invoked with the newly created sales order as payload.
Creation of new sales orders
Another use case for the API is the creation of new sales orders. This can, for example, be used to inject orders received by a third-party shop system into the enfore platform.
Note that sales orders cannot be created directly. Instead, one must first create a sales order draft and the "convert" the draft into a sales order.
Updating status of sales order
Once a sales order has been created (either via the API or via the enfore POS client application), the API provides ways to modify the status of the sales order as well as of related object such as productions and fulillments.
Note that not all status transitions are valid, for example a sales order with a delivery (fulfillment) in status PENDING
cannot be set to status COMPLETED
. See the domain model documentation for more information on the different status types and how they interact.
Domain model
Introduction (Spec)
This documentation describes the high-level architecture of the enfore Sales Orders API.
The enfore Sales Orders API is provided by the enfore platform to be convenient to use for managing sales orders in the "Retail" vertical, for example when integrating with an external system such as SAP or Dynamics.
Use cases
The aim of the SalesOrders-API is to be useful for implementing use cases related to sales in the "Retail" vertical.
Initially, the API will only support a small set of use cases:
- Fetching sales order information
- Receiving notifications about newly created sales orders
- Creation of new sales orders
- Updating status of sales orders (and related entities such as productions and fulfillments)
Fetching sales order information
A main use case of the SalesOrders-API is the ability to page through existing sales orders.
For paging through existing sales orders, the SalesOrders-API provides the endpoint GET /org/{org-id}/sales-orders/
. That endpoint follows the normal paging-pattern of the enfore-APIs in that the client must specifiy a time-window via from
and to
and can then page within that window using offset
and limit
.
For example, the following requests can be used to access the first 200 sales orders whose transaction timestamp lies between 1pm and 3pm CEST on May 1st 2019:
GET /org/{org-id}/sales-orders/?from=2017-05-01T11%3A00%3A00Z&to=2017-05-01T13%3A00%3A00Z&limit=50&offset=0
(fetches items 0 to 49)GET /org/{org-id}/sales-orders/?from=2017-05-01T11%3A00%3A00Z&to=2017-05-01T13%3A00%3A00Z&limit=50&offset=50
(fetches items 50 to 99)GET /org/{org-id}/sales-orders/?from=2017-05-01T11%3A00%3A00Z&to=2017-05-01T13%3A00%3A00Z&limit=50&offset=100
(fetches items 100 to 149)GET /org/{org-id}/sales-orders/?from=2017-05-01T11%3A00%3A00Z&to=2017-05-01T13%3A00%3A00Z&limit=50&offset=150
(fetches items 150 to 199)
Receiving notifications about newly created sales orders
Often times, clients are interested in sales orders that are newly created (for example via the enforePOS client UI). To not have to continously poll the paging endpoint, the sales order API provides a way to register for newly created sales orders via the subscription API.
The event type for newly created sales orders is SALES_ORDERS_NEW
.
Whenever a new sales order is created, the registered callback will be invoked with the newly created sales order as payload.
Creation of new sales orders
Another use case for the API is the creation of new sales orders. This can, for example, be used to inject orders received by a third-party shop system into the enfore platform.
Note that sales orders cannot be created directly. Instead, one must first create a sales order draft, that can be processed on the enfore platform to become a sales order.
Updating status of sales order
Once a sales order has been created (either via the API or via the enfore POS client application), the API provides ways to modify the status of the sales order as well as of related object such as productions and fulillments.
Note that not all status transitions are valid, for example a sales order with a delivery (fulfillment) in status PENDING
cannot be set to status COMPLETED
. See the domain model documentation for more information on the different status types and how they interact.
Domain model
The domain model of the SalesOrders-API consists of the core object types SalesOrderDraft
, SalesOrder
, Production
, Pickup
and Delivery
.
In essence, a SalesOrderDraft
represents an shopping basket in preparation. Once the preparation is done, the draft gets finalized and becomes an active shopping basket. In that state the enfore platform, e.g. the enforePOS application, can process it to become a SalesOrder
which tracks the Production
and fulfillment (Pickup
, Shipment
) of the items.
SalesOrderDraft
A SalesOrderDraft
represents an active shopping basket in preparation. It can be (almost) freely modified. For example, items may be added or removed and different fulfillment options may be added, removed or updated as necessary.
Sales order draft lifecycle
A sales order draft is essentially a helper object for the creation of a sales order. The normal flow is that a sales order draft is created, items and fulfillments are added, removed, and modified, and then it is finalized to become an active shopping basket. In that state it is SalesOrderDraft anymore.
For example, to create a sales order for two products and with some customer information, the following endpoints would be called (payload/response omitted for brevity):
POST /org/{org-id}/sales-order-drafts/
- create a new draftPUT /org/{org-id}/sales-order-drafts/{sales-order-draft-id}/customer
- add customer informationPOST /org/{org-id}/sales-order-drafts/{sales-order-draft-id}/items
- add first itemPOST /org/{org-id}/sales-order-drafts/{sales-order-draft-id}/items
- add second itemPOST /org/{org-id}/sales-order-drafts/{sales-order-draft-id}/patch
withSalesOrderDraftFinalizationRequest
- finalize draft
Once created, a sales order draft exists until it either is explicitly deleted (via DELETE /org/{org-id}/sales-order-drafts/{sales-order-draft-id}
) or until it is finalized.
Operations on a sales order draft
Between the creation of a new sales order draft and its explicit deletion or finalization, clients may perform any number of operations on the draft.
The order of those operations is not limited by the API. For example, one could create a draft, add two items, update the first item, add customer information, add a pickup, remove the second item, update the customer information and only then finalize the draft.
The only limitations are defined by the need to reference items from the fulfillments (pickups and deliveries). While it is possible to add an "empty" fulfillment (one without any items), for an item to be part of a fulfillment, the item has to first exist.
Note that the deletion of an item will remove all references to the item from any fulfillments or other items that currently reference it. This may cause a fulfillment to become "empty". Such fulfillments are removed when the draft is finalized.
SalesOrder
An SalesOrder
represents a closed shopping basket or finalized list of items that are to be sold. It may still be unconfirmed and can be canceled, but its "content" (i.e., the items and additional information such as the customer) cannot be changed anymore.
Sales order lifecycle
The lifecycle of a sales order is relatively simple. The initial state is NEW
and there are the three final states REJECTED
, COMPLETED
, and CANCELED
. In between, a sales order may be put ON_HOLD
at any time:
The meaning of the states is as follows:
State | Meaning |
---|---|
NEW | The order has been newly create and must not be either confirmed or rejected by the merchant. |
CONFIRMED | The order has been confirmed by the merchant which means that he intends to fulfill it. |
REJECTED | The order has been rejected by the merchant which means that he does not accept it and it won't be fulfilled. |
IN_PROCESS | The order is being fulfilled. That is, items are produced and/or pickups/deliveries are active/outstanding. |
COMPLETED | The order has been completed. That is, all items have been produced and fulfilled (picked up or delivery) as necessary. |
CANCELED | The order has been canceled. That is, it once was confirmed any may have been partially fulfilled but for some reason it was decided to not complete it. |
ON_HOLD | The order has temporarily been put "on hold". That is, the order and its depending items (productions, pickups, deliveries) cannot progress further except by returning to the previous state or by being canceled. |
Related objects
A sales order might have related object such as productions and fulfillments. Those related objects are separate resource in the API with their own status but the sales order holds links to the objects it is related to as well as an aggregated status.
The production_information
field holds a SalesOrderProductionInformation
structure that holds links to and an aggregated status for all productions related to the sales order (i.e., productions for items that are sold via the order).
The fulfillment_information
field holds a SalesOrderFulfillmentInformation
structure that holds links to and an aggregated status for all fulfillments related to the sales order (i.e., pickups and deliveries for items that are sold via the order).
The invoice_information
field holds a SalesOrderInvoiceInformation
structure that holds links to and an aggregated status for all invoices related to the sales order (i.e., invoices for the sales order or parts of the sales order).
The aggregated status only provides a rough overview on the status of the related objects:
State | Meaning |
---|---|
NOT_NECESSARY | There are no such related objects. For example, an order that is completely fulfilled "over the counter" would have NOT_NECESSARY as fulfillment_information.aggregated_status |
PENDING | There are related objects and none of them have been completed yet. |
PARTLY_COMPLETED | There are related objects and at least one of them has been completed and at least another one is not completed yet. |
COMPLETED | There are related objects and all of them are completed. |
Note that the meaning of "completed" differs depending on the type of related object. For a Production
it means that the item has been produced. For an Invoice
it means that it has been fully paid.
Also note that related objects are created when the sales order gets CONFIRMED
. That is a NEW
or REJECTED
order won't have related objects (yet).
Edge cases
While SalesOrders-API is focussed on the "retail" vertical and tries to provide a domain-specific view on the business data, the enfore platform provides some advanced features that cannot easily be mapped to this domain-specific view.
For example, for the "gastro" vertical, the platform allows "gastro orders" be be modified, even after they have already been confirmed and partly fulfilled (e.g., somone ordering a second drink). While such "gastro orders" should not be made available via the SalesOrders-API, we currently cannot easily distingush between "retail orders" and "gastro orders". Therefore, in an organization with mixed registers, the SalesOrders-API might expose such "gastro ordes". Those orders can be interacted with via the API just fine, but contrary to regular SalesOrder objects, they may exhibit mutations (e.g., an added item) that normally do not occur.
Production
An Production
represents the process of the creation of an item. For example, a "gift basket" item might need to be assembly by a staff member from other products rather than sit pre-packaged on a storage shelf.
Productions are created automatically by the system based on the configuration of the products and organization, they are not configurable via the sales order draft like fulfillments.
A production is always performed at a specific location
and covers one or multiple items
. Whether two items end up in the same production or not depends on the location as well as organization settings.
A production follows a simple state machine:
The meaning of the states is as follows:
State | Meaning |
---|---|
PENDING | Nothing has been producted yet. This is the initial state. |
PARTLY_COMPLETED | Some, but not all, of the items have been produced already. |
COMPLETED | All items have been produced. This is a final state. |
CANCELED | The production has been canceled. Items may or may not have been produced before the production was canceled. This is a final state. |
Note that a production's status cannot be modified directly. Instead, the status updates automatically when the status of a production item changes (from PENDING
to COMPLETED
or back) or when the sales order that the production belongs to gets canceled.
Pickup
A Pickup
represents the process of a customer (or someone on behalf of the customer) to pick up one or multiple items from a service location.
Pickups are created based on the SalesOrderDraftPickup
objects configured during the draft phase.
A pickup is always performed at a specific location (pickup_location
) and may be scheduled to occur during a specific time frame (pickup_time_range
).
There may be information about the collector (collector_information
), but that isn't a requirement if the business has some other way to ensure items can only be picked up by the right person (e.g., by using a pager or checking the barcode on the pickup slip).
A pickup follows a simple state machine:
The meaning of the states is as follows:
State | Meaning |
---|---|
PENDING | The pickup has not yet happend and the items are not yet prepared either. This is the initial state. |
PREPARED | The items have been prepared for pickup. |
PICKED_UP | The items have been picked up. This is a final state. |
CANCELED | The pickup has been canceled. Items may or may not have been prepared before the cancelation. This is a final state. |
Note that a pickups's status can be modified via the API (via PUT /org/{org-id}/fulfillments/pickups/{pickup-id}/status
) but that it cannot be set to CANCELED
that way. A pickup only get's canceled when the sales order that it belongs to gets canceled.
Delivery
A Delivery
represents the process of delivering one or multiple items to a customer (or some recipient specified by the customer).
Deliveries are created based on the SalesOrderDraftDelivery
objects configured during the draft phase.
A delivery is always performed from a specific location (shipping_location
) to a specific address (delivery_addres
) and may be scheduled to be delivered during a specific time frame (delivery_time_range
).
There may be more information about the collector than just the address (recipient_information
), but that is optional.
A delivery follows a simple state machine:
The meaning of the states is as follows:
State | Meaning |
---|---|
PENDING | The delivery has not yet happend and the items are not yet prepared either. This is the initial state. |
PREPARED | The items have been prepared for shipment. |
SHIPPED | The items have been shipped / handed over to the shipping carrier. |
RECEIVED | The items have been received by the customer/recipient. This is a final state. |
CANCELED | The delivery has been canceled. Items may or may not have been prepared before the cancelation. This is a final state. |
Note that a delivery's status can be modified via the API (via PUT /org/{org-id}/fulfillments/deliveries/{delivery-id}/status
) but that it cannot be set to CANCELED
that way. A delivery only get's canceled when the sales order that it belongs to gets canceled.
Introduction (Spec)
This documentation describes the high-level architecture of the enfore Staff & Access Rights API.
The enfore Staff & Access Rights API is provided by the enfore platform to allow management of staff members and their access rights without the need to use the client UI for this.
The exact set of modules and their capabilities will evolve over time, the initial API version will only support a very limited set of use cases.
Use cases
Domain model
The domain model of the staff members & access rights API consists of the core object types StaffMember
, StaffAccessLevel
and ApplicationAccessRights
. A StaffMember
is an employee of the organization. A StaffAccessLevel
is an entity that can be granted access rights and that staff members can be assigned to. ApplicationAccessRights
is the structure that holds the information what staff member and/or access level has which access rights.
Application Access Rights
The enfore platform manages application access rights on a "per application"-basis. The term "application" in the context of this API means "client application" such as "enforePOS". For this API, the application is always specified via a path parameter (in addition to the staff member/access level and the organization).
There are four distinct types of access rights:
- "Dashboard widgets" grant access to specific widgets on the application dashboard.
- "Dashboard sales channels" grant access to view data for a specific sales channel via the dashboard widgets.
- "Application workflows" grant access to specific workflows in the application (a workflow usually equals a specific sidebar menu item)
- "Application functions" grant access to specific functions of the application (a function is usually some action/operation that can be invoked as part of a workflow)
The granted rights for each of the four types are listed in the map fields of the ApplicationAccessRights
structure.
The keys of the dashboard_widgets
, application_workflows
, and application_functions
maps are access right identifiers (see the application-specific documentation appendices for those).
The keys for the dashboard_sales_channels
map are sales channel identifiers and are not depending on the application.
The values for all four of the maps ApplicationAccessRightAccessInfo
objects holding the IDs of the staff members and/or access level that are granted the right.
For example, the following structure:
{
"dashboard_widgets": {
"top_products": {
"everyone": true
},
"total_revenue": {
"access_level": "X",
"staff_members": [ "A", "B"]
},
"open_invoices": {
"staff_members": [ "A" ]
}
},
"dashboard_sales_channels": {
"hamburg": {
"access_level": "X",
"staff_members": [ "A" ]
},
"berlin": {
"access_level": "X",
"staff_members": [ "B" ]
}
},
"application_workflows": {
"sales_register": {
"access_level": "X",
"staff_members": [ "A" ]
},
"inventory_list": {
"staff_members": [ "B" ]
}
},
"application_functions": {
"void_lineitems": {
"staff_members": [ "A" ]
}
}
}
Means that:
- everyone has access to
- the dashboard widget "top_products"
- access level X has access to
- the dashboard widget "total_revenue"
- the dashboard sales channels "hamburg" and "berlin"
- the application workflow "sales_register"
- staff member A has access to
- the dashboard widgets "total_revenue" and "open_invoices"
- the dashboard sales channel "hamburg"
- the application workflow "sales_register"
- the application function "void_lineitems"
- staff member B has access to
- the dashboard widget "total_revenue"
- the dashboard sales channel "berlin"
- the application workflow "inventory_list"
Default access
When no ApplicationAccessRightAccessInfo
is given for a specific right (i.e., when the key is missing from the map), a "default access" configuration is used.
For dashboard_widgets
and dashboard_sales_channels
, the default access configuration is "no access to anyone".
For application_workflows
and application_functions
, the default access configuratin is "access to every staff member".
Appendix A - Application rights for "enforePOS" application
The only application with access rights management currently is the enforePOS application. Its application-id
is enforePOS
.
Dashboard widgets
- Module "Sales"
n4_applications_enforePOS_module_N4SalesModule__total_revenue
n4_applications_enforePOS_module_N4SalesModule__top_sales_categories
n4_applications_enforePOS_module_N4SalesModule__top_staff
n4_applications_enforePOS_module_N4SalesModule__order_average
n4_applications_enforePOS_module_N4SalesModule__revenue_by_weekday
n4_applications_enforePOS_module_N4SalesModule__revenue_by_time_of_day
n4_applications_enforePOS_module_N4SalesModule__top_items
n4_applications_enforePOS_module_N4SalesModule__flop_items
n4_applications_enforePOS_module_N4SalesModule__revenue_by_sales_channel
n4_applications_enforePOS_module_N4SalesModule__top_payment_methods
n4_applications_enforePOS_module_N4SalesModule__payment_methods
n4_applications_enforePOS_module_N4SalesModule__tip
n4_applications_enforePOS_module_N4SalesModule__inhouse_vs_take_away
n4_applications_enforePOS_module_N4SalesModule__revenue_by_tax_rate
n4_applications_enforePOS_module_N4SalesModule__top_promotions
n4_applications_enforePOS_module_N4SalesModule__granted_discounts
n4_applications_enforePOS_module_N4SalesModule__refunds
n4_applications_enforePOS_module_N4SalesModule__most_returned
n4_applications_enforePOS_module_N4SalesModule__top_options
n4_applications_enforePOS_module_N4SalesModule__escrow
n4_applications_enforePOS_module_N4SalesModule__due_invoices
n4_applications_enforePOS_module_N4SalesModule__invoices
n4_applications_enforePOS_module_N4SalesModule__open_invoices
n4_applications_enforePOS_module_N4SalesModule__sales_vouchers
n4_applications_enforePOS_module_N4SalesModule__active_sales_vouchers
n4_applications_enforePOS_module_N4SalesModule__average_revenue_per_guest
n4_applications_enforePOS_module_N4ServiceModule__cancellation_reasons
n4_applications_enforePOS_module_N4ServiceModule__cancellations_by_service
n4_applications_enforePOS_module_N4ServiceModule__no_shows
n4_applications_enforePOS_module_N4ServiceModule__no_shows_over_time
n4_applications_enforePOS_module_N4ServiceModule__reservations_by_origin
n4_applications_enforePOS_module_N4ServiceModule__service_cancellations
n4_applications_enforePOS_module_N4ServiceModule__upcoming_reservations
Application workflows
- Module "Sales"
register_reports
sales_invoicing
sales_promotions
sales_rawMaterial
sales_register
sales_reports
sales_salesCategories
sales_salesChannel
sales_salesHistory
sales_salesItems
sales_salesSettings
- Module "Reservations"
service_activeReservation
service_assignments
service_peopleAndEquipment
service_reservationHistory
service_reservationManager
service_services
service_settings
service_shiftPlanning
- Module "Customers"
customer_active
customer_allCustomers
customer_customList
customer_important
customer_inactive
customer_marketing
customer_returning
customer_settings
- Module "Order Management"
orderManagement_activeOrders
orderManagement_orderAssembly
orderManagement_orderHistory
orderManagement_settings
- Module "Inventory"
inventory_list
inventory_locations
inventory_purchasing
inventory_reconciliations
inventory_settings
inventory_stockTrading
inventory_supplier
inventory_transfers
- Module "Deliveries"
deliveries_deliveries
deliveries_incommingDeliveries
deliveries_outgoingDeliveries
deliveries_settings
deliveries_transferOrders
- Module "Settings"
businessAccount_devices
businessAccount_info
businessAccount_istaff
businessAccount_settings
businessAccount_staffplanning
settings_userRights
Application functions
- Module "Sales"
sales_acceptExpiredSalesVouchers
sales_accountPayment
sales_assignments
sales_cash_abort_drawing
sales_cash_deposit_cash_supplier
sales_cash_deposit_location
sales_cash_deposit_pettyCash
sales_cash_deposit_bank_account
sales_cash_deposit_staff
sales_cash_deposit_vault
sales_cash_deposit_expense
sales_cash_drawing_cash_supplier
sales_cash_drawing_location
sales_cash_drawing_pettyCash
sales_cash_drawing_bank_account
sales_cash_drawing_expense
sales_cash_drawing_late_at_location
sales_cash_drawing_late
sales_cash_drawing_safebag
sales_cash_drawing_staff
sales_cash_drawing_tip
sales_cash_drawing_vault
sales_cash_min_max_exceeded
sales_cash_reconciliation_at_location
sales_cash_transaction_device
sales_cash_transaction_staff
sales_changeInvoicePayments
sales_changeSalesChannelPrice
sales_checkout
sales_ExternalPayment
sales_manualCardPayment
sales_mobilePayment
sales_negativeBooking
sales_noPayoutOnLowerCashBalance
sales_payLater
sales_release_used_cash_drawer
sales_register_history
sales_registerReports_current
sales_registerReports_history
sales_registerReports_location_cashVaults_history
sales_registerReports_location_pettyCash_current
sales_registerReports_location_pettyCash_history
sales_registerReports_location_safebags_current
sales_registerReports_location_safebags_history
sales_registerReports_settings
sales_registerReports_complete_drawer
sales_registerReports_force_complete_drawer
sales_registerReports_complete_location
sales_registerReports_complete_staff_member
sales_registerReports_complete_staff_member_deputy
sales_registerReports_force_complete_other_staff_member
sales_registerReports_device_current
sales_registerReports_device_history
sales_registerReports_location_cashLedger_current
sales_registerReports_location_cashLedger_history
sales_registerReports_location_cashVaults
sales_registerReports_location_report_current
sales_registerReports_location_report_history
sales_registerReports_management
sales_registerReports_management_devices
sales_registerReports_management_staffMembers
sales_registerReports_management_locations
sales_registerReports_management_cashLedgers
sales_registerReports_management_pettyCash
sales_registerReports_management_cashVaults
sales_registerReports_management_safebags
sales_registerReports_organisation
(deprecated)sales_registerReports_reopen
- use
sales_registerReports_reopen_device
andfunctions_sales_registerReports_reopen_staff_member
instead
- use
sales_registerReports_reopen_device
sales_registerReports_reopen_staff_member
(deprecated)sales_registerReports_share
- use
sales_registerReports_email
andsales_registerReports_print
instead
- use
sales_registerReports_email
sales_registerReports_print
sales_registerReports_staffMember_current
sales_registerReports_staffMember_history
sales_registerReports_start_drawer
sales_registerReports_start_location
(deprecated)sales_register_history
sales_release_own_cash_drawer_assignment
sales_salesReports_bySalesCategory
sales_salesReports_bySalesChannel
sales_salesReports_bySalesItem
sales_salesReports_byStaffMember
(deprecated)sales_salesReports_share
- use
sales_salesReports_print
andsales_salesReports_email
instead
- use
sales_salesReports_print
sales_salesReports_email
sales_salesReports_total
sales_settings_fiscalmemory
sales_unpaidInvoicePayment
(deprecated)sales_userDefinedPriceDiscount
- use
sales_userDefinedDiscount
andsales_userDefinedPrice
instead
- use
sales_userDefinedDiscount
sales_userDefinedPrice
sales_viewProductDetails
sales_voidPayment
sales_voidLineItem
sales_voidOrder
sales_delete_subOrdersAndInvoices
- Module "Customers"
customer_changeCreditLimit
customer_chargeAccount
customer_edit
customer_lockAccountCredit
customer_payoutAccount
customer_register_show_customer
customer_register_switch_customer
customer_customerList_edit
customer_customerList_addRemoveCustomer
- Module "Inventory"
inventory_manageInventory
- Module "Reservations"
service_activeReservations_occupancy
service_activeReservations_reservations
service_reservationManager_occupancy
service_reservationManager_reservations
service_reservations_force_checkout
- Module "Deliveries"
deliveries_goodsout_confirm
deliveries_goodsout_reject
deliveries_goodsout_start_picking
deliveries_goodsout_mark_as_picked
deliveries_goodsout_start_packing
deliveries_goodsout_mark_as_packed
deliveries_goodsout_mark_as_ready_for_pickup
deliveries_goodsout_mark_as_picked_up
deliveries_goodsout_mark_as_fulfilled
deliveries_goodsout_put_on_hold
deliveries_goodsout_release_on_hold
deliveries_goodsout_create_cancellation_request
deliveries_goodsout_withdraw_cancelation_request_self_created
deliveries_goodsout_withdraw_cancelation_request_by_others
deliveries_goodsout_withdraw_cancelation_request_business_process
deliveries_goodsout_confirm_cancelation_request_self_created
deliveries_goodsout_confirm_cancelation_request_by_others
deliveries_goodsout_confirm_cancelation_request_business_process
deliveries_goodsout_return_package_to_packed
deliveries_goodsout_reset_to_confirmed
deliveries_goodsout_reopen
deliveries_goodsout_release_all_goods_and_package_material
deliveries_goodsout_remove_package
deliveries_goodsout_cancel
deliveries_goodsin_start_review
deliveries_goodsin_mark_as_completed
deliveries_goodsin_reopen_delivery
deliveries_goodsin_reset_delivery
deliveries_goodsin_reject_delivery
deliveries_goodsin_create_delivery
deliveries_goodsin_item_add
deliveries_goodsin_item_delete
deliveries_goodsin_item_unknown_received_quantity
deliveries_goodsin_item_edit_delivery_note_quantity
deliveries_goodsin_item_edit_received_quantity
deliveries_goodsin_item_resolutions_add
deliveries_goodsin_item_resolutions_edit
deliveries_goodsin_item_resolutions_book
deliveries_goodsin_item_resolutions_adjust
deliveries_goodsin_item_resolutions_revert
deliveries_goodsin_mark_as_received
deliveries_goodsin_mark_as_reviewed
deliveries_goodsin_resolution_started
deliveries_goodsin_package_add
deliveries_goodsin_package_received
deliveries_goodsin_package_verified
deliveries_goodsin_package_not_received
deliveries_goodsin_package_reset_to_advised
deliveries_goodsin_package_reset_to_received
Inverse APIs
Inverse APIs are APIs that external integrators need to implement to enable specific functionality in the enfore platform.
For example, when an external pricing engine is to be used, integrators must implement the External Pricing Engine-API.
Document Creation Service (Spec)
Introduction
While the enfore platform provides a built-in capability to generate business documents that can then be printed or shared via email/SMS, it does not allow for an in-depth customization of such documents.
For merchants that want to have full control about the design and content of the generated documents, the enfore platform allows the inclusion of an external service for the creation of documents. That service is called a "Document Creation Service" (DCS) and must implement the "enfore Document Creation Service"-API.
The DCS must be configured for the organization and the configuration must specify for which business document types the service is to be used. This allows the service to only be used for those types of documents where custom design or content are necessary and frees the implementor of the service from the need to support all 30+ business document types.
While an instance of the custom document creation service must be available via the internet from the enfore Cloud, additional instances can be configured for each POS location. Those local instances are then used by the clients running at their POS location and are thus accessed via the local network. This enables full offline capability, provided the custom document creation service does work offline.
The following diagam shows the components and their relationships in a setup with local DCS instances:
Given this setup, the main use case is the creation and printing of an invoice document as part of the checkout of a sales order:
In the "receipt step" of the enforePOS checkout & payment flow, the enforePOS client requests the creation of the invoice document (1). The request will contain the invoice data (customer, amounts, line items, etc), the output format that the generated document is expected to have (e.g., A4 PDF) as well as context information (e.g., the organization, POS location and register the client is running on).
The DCS is expected to return the generated docment binary and, optionally, some "external data" (2). The external data will be persisted in the enfore platform together with the business document data so that it can later be passed back to the DCS in case a new document binary is needed.
This re-requesting of a document binary is needed because a) a document binary for a different output format may be needed and b) the enforePOS client will not upload the generated document binary to the enfore server as port of storing the invoice data (4).
The binary is not uploaded to avoid potential problems with limited network bandwith.
Note that, as the document is not uploaded to the enfore server, a later request to send the document via email (for example), requires a new document binary to be generated:
Design decisions
The current version of the DCS API has been designed around some decisions that have been made to be able to provide a useful API but not impose excessive requirements on the implementors and users of the API.
Directly returning document binary
The current DCS API requires the service to directly return the generated document binary in the response to the "createDocument" request.
While this requires the HTTP request/response to "wait" for the document generation, it avoids the need to implement a complete "job management" API with a "create job, poll status, download result" loop.
Document creation is expected to be done in less than 2 seconds and "parked" HTTP connections do not use up much resources, so the benefit of a simpler and easier to implement and use API should outweigh the drawbacks.
As a future extension, the requirement for the direct return of the document binary may be replaced with an option to return a "job ID" instead. Additional API for querying job status and downloading the binary will then need to be added.
Not uploading document binary
The enforePOS client will not upload the document binary to the enfore server platform. Instead, the enfore server platform will (re-)request the binary from the cloud-based DCS instance.
This is done as storing and uploading the document binaries on the client side imposes significant storage and bandwith requirements. Especially for the offline-capabale enforePOS2 clients, storing and uploading 100+ documents is not feasible.
Additionally, even if the design would include the document being uploaded from the client, the enfore server platform must be able to request documents directly from the DCS as the client will only invoke the DCS if the document is to be printed as part of the checkout. If the user chooses to not print the document during checkout, no document binary will be created as that would only slow down the checkout. Instead, the document will then be created only if explicitly requested via some action in the sales history.
No support for receipt output format
The current API does not define the parameters for the receipt output format. While the DCS will need to support creation of receipt printer documents at some point, this isn't necessary for the first roll-out as only page printers are used there.
Versioning
The DCS API supports versioning in two places:
- the actual OpenAPI endpoints
- the document data structures
Those versions will get increased separately, making them semi-independent. Note that they are not fully independent as specific versions of the document data structures may only be uable with specific versions of the OpenAPI endpoints.
The "createDocument" operation
For now, the DCS-API consists of just the "createDocument" operation. For this operation, a client executes a POST
request against the DCS and provides the following information:
- Request ID
- Document Type Identifier
- Document Data
- Meta Data
- Locale
- Output Format
- (optional) External Data
- (optional) Duplicate Flag
The response is expected to be multipart/form-data
consisting of one or two parts. The mandatory part named binary
must contain the generated document binary and use content type application/octet-stream
or application/pdf
. Optionally, the DCS can provide an "external data" via a part named external_data
using content type text/plain
. The parts can be in any order, they must be identified by their Content-Disposition
header field.
For example, a "createDocument" response may look like:
content-type: multipart/form-data; boundary="BOUNDARY"
--BOUNDARY
content-type: text/plain
content-disposition: form-data; name="external_data"
This is external data...
--BOUNDARY
content-type: application/pdf
content-disposition: form-data; name="binary"
<<< binary data of PDF >>>
--BOUNDARY--
Security considerations
Offloading the creation of business document binaries to an external service requires both the enforePOS clien/enfore server platform that calls the DCS as well as the DCS that is being called to trust the other part.
The enforePOS client/enfore server platform must ensure that it is receiving document binaries from a trusted DCS source as it is unable to verify the contents of the binary (e.g., PDF). Otherwise, the system may print documents stating information that is not true.
Similarly, the DCS must ensure it only creates document binaries for a trusted client (enforePOS client or enfore server platform) as otherwise it could be used to create documents for business operations that have not happened or documents that contain data different from what actually happened.
The exact means of establishing two-way trust will still need to be worked out. Options that have been mentioned are:
- client -> DCS
- Use of client certificates
- Use of
authorization
header - add cryptographic signature in application layer (i.e., part of request)
- DCS -> client
- rely on HTTPS
- add cryptographic signature in application layer (i.e., part of response)
Domain Model (Spec)
The domain model of the External Pricing-API consists of the core object types PricingEngineContext
and PricingEngineResult
. A PricingEngineContext
is sent to the external pricing logic which is expected to return a PricingEngineResult
.
Line items
Both PricingEngineContext
and PricingEngineResult
mainly consist of items. In case of the context, those are of type PricingEngineContextLineItem
. For the result, the items are either PricingEngineResultItem
or PricingEngineResultAdditionalItem
objects.
Line item dependencies
In most cases, a line item is a stand alone item and fully describes the order position by itself. In some cases, though, a position is complex enough to need additional items to be expressed. Those items are not independent items but are depending on a "root item".
For example, consider a sales order at a burger restaurant:
Id | Dependency | Qty | Product | BasePrice | Price |
---|---|---|---|---|---|
1 | 1x | Softdrink | 3€/1x | 3€ | |
2 | 1x | Burger | 8€/1x | 8€ | |
3 | 2 / OPTION | 1x | Bacon | 1,50€/1x | 1,50€ |
4 | 2 / OPTION | 1x | Extra Cheese | 1€/1x | 1€ |
5 | 2 / OPTION | 1x | Well done | 0€/1x | 0€ |
Here, the Burger is a configurable BTO (built to order) product with a "base price" of € 8,00 and the specific sale includes the options Bacon
, Extra Cheese
, and Well done
.
Line item dependency types
The enfore platform supports different types of line item dependencies.
OPTION
The most common line item dependency type is OPTION
. An option represents a dynamic choice made by the user at runtime when adding a product an order.
Items of type OPTION
are included when computing the line item total and are shown in the client UI and printed on business documents as sub-items for their main item.
DEPOSIT
For modelling deposits, the enfore platform uses the line item dependency type DEPOSIT
. Deposits are configured on the product via the client UI and cannot be modified by the user at runtime when adding a product an order.
Items of type DEPOSIT
are included when computing the line item total and are shown in the client UI and printed on business documents as sub-items for their main item.
CONTENT_COMPOSITION
For describing the contents of a sold item, the line item dependency type CONTENT_COMPOSITION
can be used.
This dependency type is currently only usable by the external pricing logic, there is no way to create such items via the product configuration or client UI.
Items of type CONTENT_COMPOSITION
are ignored when computing the line item total and are not shown in the client UI nor printed on the generic enfore documents.
Custom document templates may choose to display those items in addition (or even in place) of the main item. When the items are to be displayed in place of the main item, care must be taken to ensure that the combined prices of the CONTENT_COMPOSITION
items match the price of the main item so that the overall price displayed stays the same. See the depdency typen PRICING_COMPOSITION
for more information.
PRICING_COMPOSITION
For describing how the price of a line item was computed, the line item dependency type PRICING_COMPOSITION
can be used.
This dependency type is currently only usable by the external pricing logic, there is no way to create such items via the product configuration or client UI.
Items of type PRICING_COMPOSITION
are ignored when computing the line item total and are not shown in the client UI nor printed on the generic enfore documents.
Custom document templates may choose to display those items in place or in addition of the main item. As those items are used to visualize the price computation, care must be taken to ensure that the combined prices of the PRICING_COMPOSITION
items match the price of the main item so that the overall price displayed stays the same.
Specifically, discounts applied to the main item must be present at the PRICING_COMPOSITION
items as well.
For example, assume a main item for the product Box of flowers
whose price is computed from the products Rose
(2x per Box) and Tulip
(5x per Box):
Id | Dependency | Qty | Product | BasePrice | Price |
---|---|---|---|---|---|
1 | 1x | Box of flowers | 9€/1x | 9€ | |
2 | 1 / PRICING_COMPOSITION | 2x | Rose | 2€/1x | 4€ |
3 | 1 / PRICING_COMPOSITION | 5x | Tulip | 1€/1x | 5€ |
Increasing the quantity on the main item must also update the quantities on the PRICING_COMPOSITION
items:
Id | Dependency | Qty | Product | BasePrice | Price |
---|---|---|---|---|---|
1 | 5x | Box of flowers | 9€/1x | 45€ | |
2 | 1 / PRICING_COMPOSITION | 10x | Rose | 2€/1x | 20€ |
3 | 1 / PRICING_COMPOSITION | 25x | Tulip | 1€/1x | 25€ |
Discounts present on the PRICING_COMPOSITION
items must be present on the main item as well:
Id | Dependency | Qty | Product | BasePrice | Discount | Price |
---|---|---|---|---|---|---|
1 | 5x | Box of flowers | 9€/1x | 10% New Customer | 40,50€ | |
2 | 1 / PRICING_COMPOSITION | 10x | Rose | 2€/1x | 10% New Customer | 18,00€ |
3 | 1 / PRICING_COMPOSITION | 25x | Tulip | 1€/1x | 10% New Customer | 22,50€ |
Note that different discounts can be present for each PRICING_COMPOSITION
item. The important thing is that all discounts from the sub items must be present on the main item as well:
Id | Dependency | Qty | Product | BasePrice | Discount | Price |
---|---|---|---|---|---|---|
1 | 5x | Box of flowers | 9€/1x | 5€ Rose Day 2€ Tulip Special | 38€ | |
2 | 1 / PRICING_COMPOSITION | 10x | Rose | 2€/1x | 5€ Rose Day | 15€ |
3 | 1 / PRICING_COMPOSITION | 25x | Tulip | 1€/1x | 2€ Tulip Special | 23€ |
Introduction (Spec)
The External Payment Method-API defines the endpoints that the enfore platform will invoke on an external server component when an external payment method is being used.
The API defines endpoints for:
- checking the availability status of the external payment method
- validating payment vouchers
- computing possible payments
- processing of payments and payouts
Types of external payment methods
The enfore platform supports three types of external payment methods:
- basic EPMs
- CUWO-based EPMs
- API-based EPMs
Basic EPMs are payment methods that do not have an actual integration with an external service, so in effect they can only be used to manually record a payment as "having been made via some external system" with no way for the enfore platform to validate this or to provide any user interface integration.
CUWO-based EPMs are backed by custom workflows, so when a payment or payout is to be performed using such an EPM, the enforePOS runtime will display the appropriate CUWO and that CUWO can perform the necessary user interaction. For more information on CUWO-based EPMs see the CUWO documentation
API-based EPMs are backed by an external service that must implement the API defined here. Whenever a payment or payout is to be performed using such an EPM, the enforePOS client will invoke the relevant endpoints of this API to process the payment/payout. Note that the API will not be directly invoked by the enforePOS client for technical reasons but that all invocations are routed through the enfore server backend.
Concepts
Availability check
Availability checks are only used for CUWO-based EPMs
The API provides the endpoint /org/{org-id}/status/
that is invoked by the enforePOS client for determining whether the external payment method is available.
For CUWO-based EPMs, this endpoint is invoked upon entering the payment flow UI to determine whether to enable the button for the EPM or not and what information to show there.
If the payment method reports that is it not available or when the status check fails (e.g., due to network error), the EPM is not available for selection by the user.
Payment vouchers & possible payments
Payment vouchers & possible payments are only supported by API-based EPMs
API-based EPMs support "payment vouchers". Payment vouchers are vouchers that can be translated into a payment. Payment vouchers are similar to discount coupons in that they have a monetary value and often (but not necessarly) have limitations/requirements that control when they can be used. For example a voucher might be defined as "2€ payment when bying 5 or more cans of beans".
The main difference between a payment voucher and a discount coupon is that a payment voucher does not reduce the invoiced amount but that, instead, it is used as a payment.
For example, assume a basket consisting of "10x cans of beans" where each can costs 5€. The line item/basket total would then be 50€. With a discount coupon of "2€ discount when bying 5 or more cans of beans", the line item/basket total would be reduced to 48€ and the user would receive an invoice with a total amount of 48€ and a "due amount" of 48€. With a payment voucher instead, the line item/basket total would stay at 50€ and the user would receive an invoice with a total amount of 50€, a 2€ payment, and a "due amount" of 48€.
During sales process
When the user presents a payment voucher during the sales process, the enforePOS runtime will use the EPM-API to validate it. The validation will only receive the voucher code and a payment context (POS location, device, customer, etc.) but not the full basket. Thus, it will not be able to validate any constraints like "when bying 5 or more of ..." that the voucher might have. The validation is to determine whether the voucher is valid "in general" but it cannot yet determine if it can actually be used.
Once the user chooses to start the check-out, the enfore system will invoke the API's "compute possible payments" endpoint. That endpoint receives all validated vouchers, the payment context, and the full basket. Based on that information, the external service is to compute what payments are actually possible.
Note that the computation of "possible payments" can also be configured to be done when no payment vouchers have been scanned. That way, payments can be generated based on additional information available only to the external service (e.g., vouchers activated via a mobile app).
After the "possible payments" have been computed, they are presented to the user as part of the checkout process. There, the user can check if the computed payments match his expectations and may deselect payments he does not want to perform or re-trigger computation (e.g., in case the first computation failed).
Once the user confirms the possible payments and enters the payment part of the checkout flow, the enfore system uses the "process payment" endpoints of the API to process the selected possible payments.
The following diagram shows how this fits into the UI flow for sales in the enforePOS register:
(*) Note that "processing a payment" may consist of multiple API calls
When voiding items
When voiding items from an invoice that was at least partially paid with payments returned from the "compute payments" API endpoint (that is, payments based on payment vouchers or some other configuration inherent to the EPM), it may be that the reason for those computed payments no longer exists. If that is the case, the customer must not receive the full payout amount. Instead, a part of or the full amount of the computed payment may need to be turned into a system-driven payout to offset the no-longer-valid payment.
For example, assume a customer did by "10x cans of beans" for 5€ each and used payment voucher woth 2€ with the limitation "when bying 5 or more cans of beans". The invoice total would be 50€ with 2€ paid via the voucher and 48€ paid by other means (let's assume cash for the sake of the example):
If the customer now returns all the cans for some reason, a credit memo for this would have a total of 50€ as that was the invoice amount. But the user should not receive a payout for the full 50€ as the would mean that he gained 2€ since he originally only paid 48€ from his own money.
Instead, the payment that resulted from the voucher must be offset by a special "offset payout" to reduce the remaining amount to be paid out to what the customer originally paid:
Note that it is not possible to simply revert the initial voucher payment. Doing so would just cause the original invoice to suddenly have an open due amount. And it would not do anything for the credit memo, whch would still need to be paid out in full.
It is also not possible to simply reduce the credit memo amount as that would imbalance the tax amounts of the invoice and the credit memo, causing a loss for the merchant.
In case of a partial void (e.g., customer returned only 4 or 6 cans), a decision must be made whether the voucher payment needs to be offset by an offset payout and to what amount.
For example, returning just 4 cans could mean no offset payout is necessary as the limitation of the voucher is still met. Returning 6 cans clearly causes the voucher limitation to no longer be met, but whether the full 2€ must be offset or just some part of the 2€ is a decision that depends on the voucher's conditions.
As the enfore platform cannot determine whether a void requires an offset payout and what the amount should be, the EPM API is invoked to let the EPM logic decide. This is done by invoking the "compute necessary offset payouts for void" endpoint. The EPM is expected to return the offset payouts that must be performed for the void. Those payouts will then be processed using the "grant payout" API endpoint like normal payouts with the addition of a reference to the original payment being passed as parameter. Additionally, the "compute necessary offset payouts for void" endpoint can add "external data" to the computed payouts that will also be passed as input to the "grant payout" endpoint.
The "compute necessary offset payouts for void" endpoint will not only receive the void basket but also the original sales basket items (including information about previously voided units) and the original EPM payments (including information about previously performed offset payouts). Note that any payments of the original invoice not performed via the EPM are not passed to the EPM for security and privacy reasons.
The following diagram shows how this fits into the UI flow for voiding an invoice that was at least partially paid by voucher payments via the enforePOS register:
(*) Note that "processing a payout" may consist of multiple API calls
Payment processing
In general, the enfore platform processes payments as a "payment request" that goes through the following steps:
- Started - newly create PR, payment amount has not been reserved or moved yet
- Authorized - payment amount has been reserved/approved (e.g., by a payment provider)
- Captured - payment amount has been granted/transferred to the merchant
- Booked - payment amount has been booked into the merchant's accounting system
Additionally, payment requests can "Fail" or become "Canceled".
Also note that not every payment method supports the distinction between "Authorized" and "Captured". For example, a basic cash payment does not have a "Authorization" stage but is immediately "Captured" when the customer has handed over the money.
Note that transitions from "Authorized" or "Captured" to "Canceled" only occur after a previous authorization/capture of money has been released/reverted. And that transitions from "Authorized" or "Captured" to "Failed" only occur after that release/reversal failed.
For the built-in payment methods, the enfore platform contains the logic to perform the operations necessary to transition the states of the payment/payout requests. For API-based external payment methods, that logic must be provided by the external service and exposed via the endpoints defined in the API:
- for payments
/org/{org-id}/external-payments/payment-processing/authorize-or-capture-payment
/org/{org-id}/external-payments/payment-processing/capture-payment
/org/{org-id}/external-payments/payment-processing/cancel-payment
- for payouts
/org/{org-id}/external-payments/payment-processing/revert-payment
/org/{org-id}/external-payments/payment-processing/grant-payout
/org/{org-id}/external-payments/payment-processing/cancel-payout
For payments, the possible activity flow looks like this:
For payouts, the flow is similar:
Note that the flows are almost identical to the ones defined for CUWO-based EPM with the exception that API-based EPMs do not define a "cancelled" result for the different operations. An operation can either succeed or fail, as there is no human interaction that could trigger a cancellation.
External Loyalty Programs (Spec)
To support loyalty programs managed as external service, the enfore platform provide "External Loyalty Programs" (in short: ELP). They are managed inside the platform as a new kind of Promotion.
This document describes the general workflows and how they are supported by the enfore platform.
Requirements and Context
Loyalty Programs
Loyalty Programs reward purchases and maybe other customer actions by giving benefits, most often expressed as "points" that can be collected by customers. The customer can redeem the points and get coupons, vouchers or goods in return, or he can use the points to pay orders by converting the points into a monetary value.
enforePOS allows all our customers to create Loyalty Programs as "Promotions". In these Loyalty Programs they can create rules how points are earned and other rules how these points can be converted into coupons or vouchers.
There are many existing loyalty programs outside where service providers (e.g. Payback) offer their services through their APIs and businesses have connected their IT backends to these services. The enfore platform defines an interface that allows to integrate these backend solutions in the enfore platform workflows, especially the enforePOS applciation.
The goal of this integration is to externalize all procedures that require an interaction with the loyalty program service provider, but store all relevant results from these actions in the enfore data objects (e.g. orders and invoices).
How External Loyalty Programs services work
In an external Loyalty Program a service provider like e.g. Payback offers merchants to manage customer accounts for them that collect points. The customers of the merchants register at the service provider and get a card from him. When a customer presents his card at any of the merchants that participates in the program, the customer can get "points" for his purchase. The points are submitted to the service provider and stored in the account maintained for this customer. The customer then can redeem his points at any merchant that participates in the program later.
An external program is more complex than the loyalty programs inside the enfore platform because more than two parties are involved here:
- the service provider
- the customer
- the merchant that grants the points
- the merchant where the points are redeemed
The service provider settles the points granted by a merchant with the points redeemed at the merchant in regular clearings for a fixed time range. Simply spoken, if a merchant granted more points than he redeemed in that time range, he has to pay something to the service provider. OTOH merchants with the opposite balance will receive money. This is important for the understanding of how the accounting for these points works.
When points are redeemed, because a customer used it to pay for a basket at a particular merchant, this merchant gets less money for his sale because the customer paid less. The clearing with the service provider will compensate him for that loss. In return the merchant who granted the redeemed points now can claim a revenue decrease for the original sale for that the points have been granted.
To make that process work, a merchant that grants points must not only tell the service provider how many points have been granted, but also for which sale. The service provider will keep that information and redeems points with a "first in, first out" policy. He will notify the merchant who granted the points when it is time to claim a revenue reduction and for which sale.
According to latest German jurisdiction, the point in time when the merchant can account for the revenue decrease is not the time of clearing, but the time of redemption, because the latter is the time where the customer experiences the reduction.
Sidestep: a revenue reduction can be claimed only if the loyalty program enables the customer to redeem points by paying with them or by getting a cashback. If the loyalty program only offers other non-monetary bonus, no revenue reduction is possible.
Integration API for ELP
The ELP integration in the enfore platform supports collection of points for External Loyalty Programs and payments using such points. It does not support other kinds of points redemption. In result, the integration API must support several functions:
- Verify ELP subscriptions
Verification of subscription usually happens through scanning a code on an ELP subscription card and sending the identifier from that card to the service provider through the ELP API. In case of success the call may return contact data and status information about the member. - Verify validity of ELP coupons (optional)
Loyalty Programs might issue printed coupons that give extra points when presented, depending on the content of the basket. The ELP API allows to verify if presented coupons are valid. This functionality is optional for ELP that do not support coupons. - Calculate loyalty points for sales
Sales are as represented "baskets" of sales items. enfore uses the ELP API to calculate points for them. The API call input is a suitable abstraction of the basket together with the customer identifier and all coupon identifiers that have been collected. - Book granted loyalty points on sales
At a certain point in time the calculated points for a basket will be booked for the customer using an ELP API call. - Revoke loyalty points on refunds (optional)
In reverse to the booking of granted points, points can be revoked from the customer balance when the sale that yielded the points is refunded partially or in full. This functionality is optional for ELP that do not require to revoke points for refunds. - Report usage of couponts (optional) When coupons are supported, all coupons that have been used for the final calculation stored in the created invoice must be reported as "used" so that the ELP can devalue them if necessary
Each API call is expected to either return a "success" or a "failure" result. The latter must be returned only if the call could not be executed for whatever reason, an empty result or a rejected verification are expected to be returned as a "success".
The execution of a payment using loyalty points and the retrieval of the points available for that is not part of the ELP API, it used the API for external payments. The configuration of the ELP must link to the EPM used for such payments.
Solution Outline
There are two workflows to discuss here: sale and refund. The latter is only about returns via invoice corrections (credit memos) here, not about returns as "minus bookings" in the register.
Diagram for sale interaction
The following picture shows a typical ELP interaction for sales in the enforePOS register:
(*) depending in the ELP configuration payments might be required before points are granted
Diagram for refund interaction
A typical return workflow:
Discussion of individual steps
Verify ELP subscriptions and coupons
ELP subscription cards and ELP coupons are usually registered with a scanner. For the enforePOS application this is different to other "external scan code" use cases as e.g. external coupons created by the enfore customer, as here we haven't imported objects beforehand to that the scan code could be resolved.
So the identification and acceptance needs to happen in a context where it is known that an ELP ID card or an ELP coupon shall be scanned. The context can be created in two ways:
- by the ELP by defining possible ranges for the identifiers in its configuration
- by enfore by having a dedicated button for scanning an ID card
Once a scanned card has been accepted by either method, the "verifySubscription" and "verifyCoupon" calls in the ELP API will be used to verify the input. The ELP API is expected to return a "success" even if the subscription or coupon could not be identified. In that case a status information in the result will report the details. A "failure" return value is expected only when the verification itself could not be done for whatever reason.
The enforePOS register must allow to scan the ID cards and coupons at any time, even in the checkout flow. Repeated scanning of different ID cards will be handled in a way that always the latest scan will win.
Some loyalty programs want the cashier to ask their customers if they are members. enforePOS will show a corresponding hint to the cashier if the ELP configuration requires that.
The call for the card verification optionally can return the number of points available and the points available for redemption.
Calculate loyalty points for sales
Once the pricing calculations for a basket have been finished, ELP loyalty points could be calculated too by using the "calculateBasket" call in the ELP API. For performance reasons we don't do this call after each change in the basket. By default enforePOS will do the call at least just before the checkout, but we can have more actions or flows where a call could be needed.
The call must return not only the calculated points, but also the coupons used for them.
As the result of that calculation does not influence the payment flow, we can do the final call in the background and only must make sure that we get the results before we create the invoice, as the final calculation result is stored in the created invoice. The resulting points will be booked in a separate call.
Usually the "calculateBasket" call gets the customer ELP ID as input, but it is also possible to do the call without that ("if you where subscribed ...").
When the basket gets checked out, the result of the final "calculateBasket" call is stored in the created invoice. We also store the time of the call in the result.
The calculation result is stored even if the call failed, so that the failure is documented. If the calculation failed because of problems with the ELP service, the stored data allows to catch up on this later.
The "calculateBasket" call can return ELP related information to be printed on invoice or receipt documents as textual data in a list of "AdditionalInvoiceInformation" objects.
Book loyalty points for sales
Points will be booked through the "grantPoints" call of the ELP API when the invoice got processed or paid completely, depending on the ELP configuration. This will add the granted points to the balance of the customer and it will also cause a financial penalty for the merchant that will be realized in the next settlement with the service provider (that is not subject to any business process in the enfore platform).
The result of the "grantPoints" call is stored in the "loyaltyProgramResults" of the invoice, together with the time of the call.
The result is stored even if the call failed, so that the failure is documented.
If the grant call failed because of problems with the ELP service, the stored data allows to catch up on this later.
Revoke loyalty points for refunded sales
If the ELP configuration asks for points revocations in case of refunds, the "revokeGrantedPoints" call in the ELP API will be used when the credit memo is processed by the enfore platform.
A points revocation will subtract these points from the balance of the customer and it will also cause a financial refund for the merchant that will be realized in the next settlement with the service provider (that is not subject to any business process in the enfore platform).
It's up to the ELP implementation to decide whether the revocation will actually be executed or the request will be ignored for whatever reason (points too old, balance is empty etc.). If the revocation request could be executed and led to a defined result, the call is expected to return a "success" result.
The result of the "revokeGrantedPoints" call is stored in the "loyaltyProgramResults" of the debit IncomeTransaction. We also store the time of the call in the result.
The result is stored even if the call failed, so that the failure is documented. If the revocation failed because of problems with the ELP service, the stored data allows to catch up on this later.
Reporting coupon usage
Coupons can give additional loyalty points to customers. ELP may support printed coupons as described here that are presented and scanned at the point of sale. The enfore platform will send them along the basket in the calculation calls. Other coupons might be activated by the customer using different means, e.g. an app on a phone that is issued by the ELP service provider and allows to activate "electronic" coupons on the customer's demand.
The calculation call in the ELP API is expected to return identifiers of all used coupons, the printed coupons as well as the electronic coupons. The enfore platform will store the whole list in its invoices and also reports them as used when the points are granted.
If the ELP is configured to expect granting of points to happen only if the invoice is paid, coupons will nevertheless be reported as "used" already when the invoice is processed. If the invoice is voided before it got paid, points will never be granted for that invoice and so the enfore platform will send a "restoreUsedCoupons" call to the ELP API that can restore the coupons, if the ELP supports this.
Introduction (Spec)
TODO
Introduction (Spec)
The External Contact Management-API defines the endpoints that the enfore platform will invoke on an external server component when an external contact management is enabled and the user performs specific actions.
The API defines endpoints for:
- resolving a scanned identifier to a contact
- determining what payment options a customer can use
Use cases
Resolve contact
The POST /org/{org-id}/external-contact-management/contacts/resolve
is for determining the contact identified by a scanned contact identifier.
The endpoint will receive the scanned identifier, the purpose identifying what type of contact is expected and the usual context information (sales channel/location, device used, etc.). If the passed identifier matches a contact, the endpoint must return a "contact information" structure with data about the customer. As the API is being used when external customer management is enabled, the returned structure should be an AnonymousContactInformation
or PlaceholderContactInformation
.
Check available payment options
The POST /org/{org-id}/external-contact-management/customers/check-available-payment-options
endpoint is used for determining what payment options are available to a customer.
The endpoint will receive information about the customer and the usual context information and is expected to return, for each payment option, whether it is enabled and up to what limit. The check currently only covers the pament options "later", "invoice", and "deferred invoicing", but that may be extended later if necessary.