diff --git a/packages/bookshop/app/common.cds b/packages/bookshop/app/common.cds index 0f1fd1f6..6b2160d7 100644 --- a/packages/bookshop/app/common.cds +++ b/packages/bookshop/app/common.cds @@ -73,12 +73,12 @@ annotate my.Authors with { name @title:'{i18n>AuthorName}'; } -annotate my.ShippingAddresses with { - AddressID @title:'{i18n>AddressID}'; +annotate my.Addresses with { + ID @title:'{i18n>AddressID}'; BusinessPartner @title:'{i18n>BusinessPartner}'; - cityName @title:'{i18n>cityName}' @readonly; - streetName @title:'{i18n>streetName}' @readonly; - postalCode @title:'{i18n>postalCode}' @readonly; - country @title:'{i18n>country}' @readonly; - houseNumber @title:'{i18n>houseNumber}' @readonly; + @readonly cityName @title:'{i18n>cityName}'; + @readonly streetName @title:'{i18n>streetName}'; + @readonly postalCode @title:'{i18n>postalCode}'; + @readonly country @title:'{i18n>country}'; + @readonly houseNumber @title:'{i18n>houseNumber}'; } \ No newline at end of file diff --git a/packages/bookshop/app/orders/fiori-service.cds b/packages/bookshop/app/orders/fiori-service.cds index ab84ef12..78fc9754 100644 --- a/packages/bookshop/app/orders/fiori-service.cds +++ b/packages/bookshop/app/orders/fiori-service.cds @@ -29,8 +29,8 @@ annotate AdminService.Orders with { Parameters : [ { $Type : 'Common.ValueListParameterOut', - LocalDataProperty : 'shippingAddress_AddressID', - ValueListProperty : 'AddressID' + LocalDataProperty : 'shippingAddress_ID', + ValueListProperty : 'ID' }, { $Type : 'Common.ValueListParameterOut', @@ -110,7 +110,7 @@ annotate AdminService.Orders with @(UI : { }, {Value : OrderNo}, { - Value : 'shippingAddress', + Value : 'shippingAddress_ID', Label : 'Address ID' } ], @@ -157,7 +157,7 @@ annotate AdminService.Orders with @(UI : { ]}, FieldGroup #ShippingAddress : {Data : [ { - Value : shippingAddress_AddressID, + Value : shippingAddress_ID, Label : '{i18n>shippingAddress}' }, { @@ -180,7 +180,7 @@ annotate AdminService.Orders with @(UI : { }, Common.SideEffects : { EffectTypes : #ValueChange, - SourceProperties : [shippingAddress_AddressID], + SourceProperties : [shippingAddress_ID], TargetProperties : [ shippingAddress.country, shippingAddress.houseNumber, diff --git a/packages/bookshop/db/schema.cds b/packages/bookshop/db/schema.cds index 6afa0a07..55363901 100644 --- a/packages/bookshop/db/schema.cds +++ b/packages/bookshop/db/schema.cds @@ -1,6 +1,5 @@ namespace sap.capire.bookshop; using { Currency, managed, cuid } from '@sap/cds/common'; -using { API_BUSINESS_PARTNER as external } from '../srv/external/API_BUSINESS_PARTNER'; entity Books : managed { key ID : Integer; @@ -8,7 +7,7 @@ entity Books : managed { descr : localized String(1111); author : Association to Authors; stock : Integer; - price : Decimal(9,2); + price : Decimal; currency : Currency; } @@ -25,24 +24,12 @@ entity Authors : managed { entity Orders : cuid, managed { OrderNo : String @title:'Order Number'; //> readable key Items : Composition of many OrderItems on Items.parent = $self; - total : Decimal(9,2) @readonly; + total : Decimal @readonly; currency : Currency; - shippingAddress : Association to one ShippingAddresses; } entity OrderItems : cuid { parent : Association to Orders; book : Association to Books; amount : Integer; - netAmount : Decimal(9,2); + netAmount : Decimal; } - -@cds.persistence: {table, skip: false} -entity ShippingAddresses as projection on external.A_BusinessPartnerAddress { - key AddressID, - key BusinessPartner, - Country as country, - CityName as cityName, - PostalCode as postalCode, - StreetName as streetName, - HouseNumber as houseNumber -} \ No newline at end of file diff --git a/packages/bookshop/package.json b/packages/bookshop/package.json index 3aa3368c..586d89ad 100644 --- a/packages/bookshop/package.json +++ b/packages/bookshop/package.json @@ -18,26 +18,12 @@ "API_BUSINESS_PARTNER": { "kind": "odata", "model": "srv/external/API_BUSINESS_PARTNER", - "credentials": { + "--credentials": { ">>": "should go to bindings !!!", "destination": "cap-api532", "prefix": "sap/S4HANAOD/c532/BO" } - }, - "messaging": { - "kind": "enterprise-messaging" - } - }, - "auth": { - "passport": { - "strategy": "mock", - "users": { - "alice": { - "roles": ["admin"], - "password": "secret", - "ID": "ALICE" - } - } } } + } } diff --git a/packages/bookshop/srv/admin-service.cds b/packages/bookshop/srv/admin-service.cds index 1821e4b7..dd3f7767 100644 --- a/packages/bookshop/srv/admin-service.cds +++ b/packages/bookshop/srv/admin-service.cds @@ -4,7 +4,6 @@ service AdminService @(requires:'admin') { entity Books as projection on my.Books; entity Authors as projection on my.Authors; entity Orders as select from my.Orders; - entity Addresses as projection on my.ShippingAddresses; } // Enable Fiori Draft for Orders diff --git a/packages/bookshop/srv/admin-service.js b/packages/bookshop/srv/admin-service.js index 39d7b578..f63ae989 100644 --- a/packages/bookshop/srv/admin-service.js +++ b/packages/bookshop/srv/admin-service.js @@ -1,131 +1,98 @@ const cds = require('@sap/cds') -const { Books, ShippingAddresses } = cds.entities -const bupaSrv = cds.connect.to('API_BUSINESS_PARTNER') +// We are mashing up three services... +const admin = cds.connect.to ('AdminService') +const bupa = cds.connect.to('API_BUSINESS_PARTNER') +const db = cds.connect.to('db') -const _diff = (obj1, obj2) => - Object.keys(obj1).reduce( - (res, curr) => - obj1[curr] === obj2[curr] ? res : (res[curr] = obj2[curr]) && res, - {} +// Reflected entities for local database +const { Books, Addresses } = db.entities + +// Fetch current user's addresses from S/4 for ValueHelp. +module.exports = (admin => { + admin.on ('READ', 'usersAddresses', async (req) => { + // const UsersAddresses = req.query.from (Addresses) .where ({ BusinessPartner: req.user.id }) + // FIXME: Again that absolutely useless error message: + // [2019-12-16T20:30:14.106Z | ERROR | 1940862]: The server does not support the functionality required to fulfill the request + // FIXME: Even worse: click Orders Edit -> + // [2019-12-16T20:38:52.918Z | WARNING | 1575675]: Not Found + const { A_BusinessPartnerAddress:Addresses } = bupa.entities + const UsersAddresses = SELECT.from (Addresses, a => { + a.AddressID.as('ID'), + a.BusinessPartner, + a.Country.as('country'), + a.CityName.as('cityName'), + a.PostalCode.as('postalCode'), + a.StreetName.as('streetName'), + a.HouseNumber.as('houseNumber') + }) .where ({ BusinessPartner: req.user.id }) + return bupa.transaction(req) .run (UsersAddresses) // TODO: I'd like to write .read instead of .run + }) +}) + +// Replicate chosen addresses from S/4 when filing orders. +admin.before ('PATCH', 'Orders', async (req) => { + const ID = req.data.shippingAddress_ID; if (!ID) return //> something else + const address = await bupa.tx(req) .run ( + SELECT.one.from(Addresses).where({ + ID, BusinessPartner: req.user.id + }) + ) + if (address) return db.tx(req) .upsert (Addresses) .entries (address) +}) + +// Update local replicas when sources change in S/4. +bupa.on ('BusinessPartner/Changed', async (msg) => { + console.log('>> received:', msg.data) + + const BusinessPartner = msg.data.KEY[0].BUSINESSPARTNER //> .KEY[0] >> revisit w/ Oliver + + // fetch affected entries from local replicas + const local = db.transaction (msg) + const replicas = await local.read (Addresses) .where ({BusinessPartner}) + + // skip if not affected + if (replicas.length === 0) return + + // fetch changed data from S/4 -> might be less than local due to deletes + const changed = await bupa.tx(msg).read (Addresses) .where ({ + BusinessPartner, ID: replicas.map(a => a.ID) // where in + }) + + // update local replicas with changes from remote + return local.run (changed.map (a => + UPDATE (Addresses) .with(a) .where ({ ID: a.ID }) + )) + +}) + +// Validate incoming orders and reduce books' stocks. +admin.before ('CREATE', 'Orders', async (req) => { + + const { Items } = req.data + + // validate input... + if (!Items || Items.length === 0) + return req.reject ('Please order at least one item.') + if (!req.data.shippingAddress_ID) return req.reject ( + 'Please enter a valid shpping address.', + 'shippingAddress_ID' ) -const _queriesToUpdateDifferences = (ownAddresses, remoteAddresses) => - ownAddresses - .map(ownAddress => { - const remoteAddress = remoteAddresses.find( - address => - address.BusinessPartner === ownAddress.BusinessPartner && - address.AddressID === ownAddress.AddressID - ) - if (remoteAddress) { - const diff = _diff(ownAddress, remoteAddress) - if (Object.keys(diff).length) { - return UPDATE(ShippingAddresses) - .set(diff) - .where({ - BusinessPartner: ownAddress.BusinessPartner, - AddressID: ownAddress.AddressID - }) - } - } - }) - .filter(el => el) + // reduce stock on ordered books... + const all = await db.tx(req) .run (Items.map (each => + UPDATE (Books) .set ('stock -=', each.amount) + .where ('ID =', each.book_ID) .and ('stock >=', each.amount) + )) + all.forEach ((affectedRows,i) => affectedRows > 0 || req.error (409, + `${Items[i].amount} exceeds stock for book #${Items[i].book_ID}` + )) -bupaSrv.on('Changed', 'BusinessPartner', async msg => { - console.log('>> Message:', msg.data) - - const BusinessPartner = msg.data.KEY[0].BUSINESSPARTNER - const tx = cds.transaction(msg) - const selectQuery = SELECT.from(ShippingAddresses).where({ BusinessPartner }) - - const ownAddresses = await tx.run(selectQuery) - if (ownAddresses && ownAddresses.length > 0) { - const txExt = bupaSrv.transaction(msg) - try { - const remoteAddresses = await txExt.run(selectQuery) - const queriesToUpdateDifferences = _queriesToUpdateDifferences( - ownAddresses, - remoteAddresses - ) - await tx.run(queriesToUpdateDifferences) - } catch (e) { - console.error(e) - } - } }) -async function _readAddresses (req) { - console.log('Addresses', ShippingAddresses) - const BusinessPartner = req.user.id - const txExt = bupaSrv.transaction(req) - const selectQuery = req.query.from(ShippingAddresses).where({ BusinessPartner }) - - try { - return txExt.run(selectQuery) - } catch (e) { - console.log(e) - } +// eslint-disable-next-line no-unused-vars +function _diff (a,b) { + let any, diff={} + for (let each in b) if (b[each] !== a[each]) diff[each] = b[any=each] + return any && diff } - -async function _fillAddress (req) { - if (req.data.shippingAddress_AddressID) { - const BusinessPartner = req.user.id - const txExt = bupaSrv.transaction(req) - try { - const response = await txExt.run( - SELECT.from(ShippingAddresses).where({ - AddressID: req.data.shippingAddress_AddressID, - BusinessPartner - }) - ) - if (response && response.length === 1) { - const tx = cds.transaction(req) - const insertQuery = INSERT.into(ShippingAddresses).entries(response) - await tx.run(insertQuery) - } - } catch (e) {} - } -} - -async function _reduceStock (req) { - const { Items: OrderItems } = req.data - if (OrderItems && OrderItems.length > 0) { - const all = await cds.transaction(req).run( - OrderItems.map(order => - UPDATE(Books) - .set('stock -=', order.amount) - .where('ID =', order.book_ID) - .and('stock >=', order.amount) - ) - ) - all.forEach((affectedRows, i) => { - if (affectedRows === 0) - req.error( - 409, - `${OrderItems[i].amount} exceeds stock for book #${ - OrderItems[i].book_ID - }` - ) - }) - } -} - -function _checkMandatoryParams (req) { - if (!req.data.Items || !req.data.Items.length) { - return req.reject('Please order at least one item.') - } - if (!req.data.shippingAddress_AddressID) { - return req.reject( - 'Please enter a valid shpping address.', - 'shippingAddress_AddressID' - ) - } -} - -module.exports = cds.service.impl(function () { - this.before('CREATE', 'Orders', _reduceStock) - this.before('CREATE', 'Orders', _checkMandatoryParams) - this.before('PATCH', 'Orders', _fillAddress) - this.on('READ', 'Addresses', _readAddresses) -}) diff --git a/packages/bookshop/srv/cat-service.cds b/packages/bookshop/srv/cat-service.cds index a6e0302a..24d43ae6 100644 --- a/packages/bookshop/srv/cat-service.cds +++ b/packages/bookshop/srv/cat-service.cds @@ -10,6 +10,5 @@ service CatalogService { @requires_: 'authenticated-user' @insertonly entity Orders as projection on my.Orders; - entity Addresses as projection on my.ShippingAddresses; } diff --git a/packages/bookshop/srv/external.cds b/packages/bookshop/srv/external.cds new file mode 100644 index 00000000..b8805102 --- /dev/null +++ b/packages/bookshop/srv/external.cds @@ -0,0 +1,76 @@ +using { API_BUSINESS_PARTNER as external } from './external/API_BUSINESS_PARTNER.csn'; + +/** + * Tailor the imported API to our needs... + */ +extend service API_BUSINESS_PARTNER with { + + /** + * Simplified view on external addresses + */ + entity Addresses as projection on external.A_BusinessPartnerAddress { + key AddressID as ID, + key BusinessPartner, + Country as country, + CityName as cityName, + PostalCode as postalCode, + StreetName as streetName, + HouseNumber as houseNumber + }; + + /** + * Re-modelling the event which is currently not available declaratively from S/4 + */ + // @messaging.topic:'sap/S4HANAOD/c532/BO/BusinessPartner/Changed' + // event "BusinessPartner/Changed" { + // "KEY": array of { + // BUSINESSPARTNER : external.A_BusinessPartner.BusinessPartner + // } + // } +} + + +/** + * Mashup w/ services to also serve shipping addresses + */ +using { AdminService } from './admin-service'; +extend service AdminService { + entity usersAddresses as projection on bookshop.Addresses; +} + +using { CatalogService } from './cat-service'; +extend service CatalogService { + @readonly @requires:'authenticated-user' + entity usersAddresses as projection on bookshop.Addresses; +} + + +/** + * Mashup w/ domain model for federated data access + */ +using { sap.capire.bookshop } from '../db/schema'; + +/** + * Extend Orders to maintain references to (replicated) external Addresses + */ +extend bookshop.Orders with { + shippingAddress : Association to bookshop.Addresses; +} + +/** + * Add an entity to replicate external address data for quick access, + * e.g. when displaying lists of orders. + */ +@cds.persistence:{table,skip:false} +entity sap.capire.bookshop.Addresses as SELECT from external.Addresses { *, + false as tombstone : Boolean +}; +// entity sap.capire.bookshop.Addresses as SELECT from external.A_BusinessPartnerAddress { +// key AddressID as ID, +// key BusinessPartner, +// Country as country, +// CityName as cityName, +// PostalCode as postalCode, +// StreetName as streetName, +// HouseNumber as houseNumber +// }; diff --git a/packages/bookshop/srv/external/API_BUSINESS_PARTNER.js b/packages/bookshop/srv/external/API_BUSINESS_PARTNER.js index 9f7f6c87..3a5d26c9 100644 --- a/packages/bookshop/srv/external/API_BUSINESS_PARTNER.js +++ b/packages/bookshop/srv/external/API_BUSINESS_PARTNER.js @@ -5,8 +5,8 @@ module.exports = srv => { const payload = { KEY: [{ BUSINESSPARTNER: req.data.BusinessPartner }] } - console.log('<< Message:', payload) - srv.emit('sap/S4HANAOD/c532/BO/BusinessPartner/Changed', payload) + console.log('<< emitting:', payload) + srv.emit('BusinessPartner/Changed', payload) })