I've just been watching Greg Youngs talk on Event Sourcing, but i'm confused as to where business logic fits in. A simple example:
1) Shopping Cart Created
2) Item Added
3) Item Added
4) Promotional Code - 20% Off
Is the Promotional Code calculated against the shopping carts items and the result of that stored as an event. I understand "PromotionalCodeAddedEvent" would maybe make sense, but where does the math happen? I'm thinking:
public void AddPromotionalCode(PromotionalCode code)
{
//perform calculation against shopping cart items.
//if valid
ApplyChanges(cmd);
}
Then the result doesn't end up anywhere and the Read Model would have to perform the calculations.
I'm not fully understanding the concept, any help would be great.
Something as seemingly simple as a promotional code can actually be a fairly complex use case. Primarily, this is due to the fact that a promotional code is logic that (usually) is maintained by one or more business users, whereas it also belongs inside of the domain, itself. It is non-traditional, in that sense. There are multiple ways to handle this, and what I will outline would just be my personal approach.
For the sake of argument, assume that you have a simple series of known promotional codes, such as:
X% Off Purchase, with or without a Minimum Purchase
$X Off Purchase, with or without a Minimum Purchase
We can make some assumptions, as well:
A Promotional Code has a Start Date
A Promotional Code has an End Date
The application of a promotional code can be tricky. Consider the two scenarios that we have defined. "$X Off Purchase" is comparatively simple, because it is a fixed amount. "X% Off Purchase", however, is more complex. If we only had the fixed amount, we could apply the discount to the cart as soon as any thresholds are met. With a percentage-based discount, if the user were to add two items, add a promotional code, and then add another item, the promotion would have already been "applied".
Because of this, I would personally only "attach" a promotional code to a cart. Why? The reason is that at the point of checkout, I can likely assume that a cart is going to be used to generate an order. Until that time, the cart's contents are fluid. A user's action against the cart will change the total value of the cart, as well as the total discount, assuming a non-fixed discount amount. It can also render either discount as being invalid, should a user remove one or more items from a cart and the total value of the cart fall below the threshold to apply the discount.
So, I would actually have multiple commands which would would be involved. Essentially any commands that impact the value of the cart could change the discount amount. To that end, I would be looking for the discount amount to be recalculated for the commands which:
Add an item to the cart
Remove an item from the cart
Change the quantity of items in the cart
Add a promotional code to the cart
Change the promotional code attached to the cart
Since these all are operations against a cart, I would be calculating the discount within the promotional code, itself, with the participation of the data contained in the cart. It feels like a promotional code is going to be an aggregate, going down this path. So, I would have the command handlers invoke a domain service that can provide my cart with the information that it requires. That domain service is going to load the promotional code, and I am going to be able to pass in the line items within that cart, so that the promotional code will tell me what the calculated discount would be. Then, I am going to generate the event, which contains the new value of the cart, along with the adjusted value (the discount). Going down this path, the logic for calculating a discount based upon line items within a cart is the responsibility of the promotional code.
You could put this responsibility in the cart, instead. Personally, though, I feel as if encapsulation of domain logic within the promotional code, itself, makes more sense. I had mentioned that it is likely that you will generate an order from a cart. By having the promotional code as an aggregate, and it containing the logic to apply the discount based upon the line items, we have a single truth, in how we calculate a discount for line items - whether that is in terms of a cart or in terms of an order.
You can, for example, raise a second event such as PromotionalCodeApplied which contains the computation results.
The Read Model then just have to use the pre-computed result.
I understand "PromotionalCodeAddedEvent" would maybe make sense, but where does the math happen?
It should happen in commands that perform modifications to the shopping cart. Every such command will call some method like RecalculateTotals() where all business logic will be hosted.
Consider the following pseudocode:
public void AddPromotionalCode(PromotionalCode code)
{
var #event = new PromotionalCodeAdded(code);
var amount = RecalculateTotalAmount(extraEvent: #event);
#event.TotalAmount = amount;
_eventStore.Publish(#event);
}
decimal RecalculateTotalAmount(IEvent extraEvent)
{
var relatedEventTypes = new[] { typeof(PromotionalCodeAdded), typeof(ShoppingCartCreated), typeof(ItemAdded) };
var events = _eventStore.ReadEventsOfTypes(relatedEventTypes);
var events = events.Concat(new[] { extraEvent });
//calculation logic goes here based on all related events
}
This is where I like to return events from command methods. As Alexander Langer mentioned, you'd apply the "math" and return the relevant event(s):
public PromotionalCodeApplied AddPromotionalCode(PromotionalCode code)
{
//perform calculation against shopping cart items.
var promotionalCodeApplied = new PromotionalCodeApplied(code.VoucherNumber, discountAmount, DateTime.Now);
On(promotionalCodeApplied);
return promotionalCodeApplied;
}
public void On(PromotionalCodeApplied promotionalCodeApplied)
{
_voucherNumber = promotionalCodeApplied.VoucherNumber;
_discountAmount = promotionalCodeApplied.DiscountAmount;
_discountDate = promotionalCodeApplied.DateAppllied;
}
Now your read model has access to the relevant values.
Related
I would like to change the price of a product based on the customer's selection. For example, I'm trying to build a small PDP widget to make customers able to choose the number of candles on a cake or write text on cakes and update the price accordingly. The docs only cover how to change the price by overwriting the cart's collector/processor but I don't want to use this method because of other plugins potentially overwriting the same service. So, is there are any other methods of changing the price of the products by subscribing to an event?
There are a few things you will need to consider in this one.
Firstly, you will need to save the user input data somewhere (amount of candles, text).
Possibly a separate database table that has a OneToMany relationship on cart line items. See this article. Und ya, this is also the part where you will hook into the onLineItemAdd event & save your user input to that table. You may as well also subscribe to the onLineItemUpdate for the same saving logic. You could do the same when removing the item from the cart, although that may not be necessary if you use database's "CASCADE on delete" when implementing your DB table. Meaning once the line item gets removed by the customer (deleted in the DB), your database entry gets deleted as well.
Afterwards, you can then use the extensions or otherwise called associations to pull this data on the cart page & the order pages. You can be a little more fancy here, if you look at all the frontend router calls, you will notice that Shopware sometimes passes "Criteria" class you can hook into.
public static function getSubscribedEvents(): array
{
return [
OrderRouteRequestEvent::class => 'alterCriteria',
DocumentOrderCriteriaEvent::class => 'alterCriteria',
];
}
public function alterCriteria(Event $event): void
{
if (method_exists($event, 'getCriteria')) {
$event->getCriteria()->addAssociation('lineItems.myExtension'); // check syntax, could be lineItem.
}
}
Now you can extend the twig templates to show your candles or text in the order page, cart page, document (invoice) pages.
Secondly, you will have to handle the price. This part will be easier now that you have data saved & being automatically pulled via criteria subscribers. If it's not possible to hook into those events all the time, you will still have an option to manually load your data.
I do not recommend modifying the price itself, but maybe you could look into adding a surcharge instead. Maybe this article will be helpful to understand the price flow. You could also see if there are any other plugins out there that implement surcharge logic to see it in action.
I have a Flutter app that lets users rent items from eachother with Firestore RTDB. In my rental document, I have a field status that determines the status of the rental (think of it like shipping an item, where items can have status of 'ordered', 'shipped', 'delivered' etc). My status variable is a number between 0 and 5, and each number represents a different phase. When the status variable changes, I want to notify the other user in the rental with a push notification. But I don't know which of the following methods is best.
The first way is to use a cloud function that triggers every time the rental document is updated. But I only check for the status field. It would look something like this:
exports.notify = functions.firestore.document('rentals/{rentalId}')
.onUpdate(async (snapshot, context) => {
const oldSnap = snapshot.before.data(); // previous document
const newSnap = snapshot.after.data(); // current document
// status changes from 0 to 1
if (oldSnap.status === 0 && newSnap.status === 1) {
// do something
}
})
The one downside I can think of is I would have to do another read to get the device push token of the other user. Also, for every rental document update this cloud function will trigger, and ultimately may not even need to execute in the first place
The other way would be to have a notifications collection that stores notifications, and have a cloud function that triggers when a new notification document is added. Then, on the client side, when the user taps a button, update the status in the rental as well as create a new notification document.
Firestore.instance
.collection('rentals')
.document(rentalId)
.updateData({'status': newStatus});
Firestore.instance.collection('notifications').add({
'title': title,
'body': body,
'pushToken': <TOKEN HERE>,
});
In comparison to method 1, this does an extra write instead of a read.
Which method is better?
Both approaches can technically work and are valid. Which one you choose is depending on the use-case, and (given that both can work here) on personal preference. That's why I'll simply highlight a few key differences below, and explain when I personally choose to use which one.
The first approach you describe is treating your database like a state machine, where each state and state transition has specific meaning. You then use Cloud Functions to trigger code in the state transition.
The second approach treats the database as a queue, where the presence of data indicates what needs to happen. So Cloud Functions then triggers on the simple presence of the document.
I typically use a queue based approach for production work, since it makes it very easy to see how much work is left to be done. Anything in your notifications collection is a notification that needs to be sent.
In the state-transition data model it is much harder to see this information easily. In fact, you'll need to add extra fields to the document in order to be able to get this list of "pending notifications". For example: rentals with a pending notification are rentals where the timestamp that the status changed from 0 to 1 (a field you'll need to add, e.g. status_1_timestamp) is smaller than the timestamp the last notification was sent (a field like notification_timestamp).
But I sometimes use the state transition approach too. Usually when I want to transform the existing document, or because it's just a cool use-case to show (as in most cases the Firebase/Firestore SDKs would not expose both the old and new state).
I'd probably pick the queue based approach here, but as said before: that's a personal preference for me based on the reasoning above. If those reasons don't apply to you, or you have different reasons, that can be fine too.
Scenario :
I've been trying to implement something like Amazon has on their product pages in, where the non promoted price is crossed, and the promoted price is highlighted.
I have the PROMOTIONS product option selected in the controller, and
it is returning the potencialPromotion, but it doesn't have the value
of the discount in that structure only the message (as on the
Accelerator it appears the message).
I'm in Hybris 6.6 and I looked up the DefaultPromotionEngineService.evaluate method because it is used in the Cart, and for the cart it returns the promotion actions, but the equivalent method for the product isn't returning anything.
Has anybody done this? Seems like a pretty convencional request although I know it goes against the flexibility of promotion engine.
I can use a Regex against the message to get the discount, but it's a road I don't want to take because it will end badly....
Please suggest.
Thank you
Since you haven't mentioned it before, I have answered based on Legacy Promotion, not the Promotion Engine. I'll update answer if there is a way to do the same in Promotion Engine.
It's true, OOTB there is no service which gives you the discount price on PDP.
I think what you can do,
Override PromotionsPopulator to get the discount value and set it to promotionData.
if (fixedPricePromotion instanceof ProductFixedPricePromotionModel)
{
PromotionPriceRowModel promotionPriceRowModel = ((ProductFixedPricePromotionModel)source).getProductFixedUnitPrice();
//TODO: covert promotionPriceRowModel to promotionPriceData
//TODO: set to promotionData
}
ProductPromotionsPopulator internally calls the above populator to convert ProductPromotionModel to data.
I think we can achieve this using the price row. We can create one more price row and display the logic correctly based on the discount price.
We allow sales to attach Prospects to an Opportunity and Quote, and once credit has qualified the prospect they promote the prospect to a customer. What we need to do is hide the "Sales Order" button on the quote, or disallow advancing the quote to a sales order.
I was hesitant to ask this- seems like it should be intuitive to figure out. I looked at the standard NetSuite button id's in NetSuite help but there wasn't one for "Sales Order". I've looked at validation logic but this isn't validation as the sales order button is shown when record is not in edit mode. If possible I'd like the solution to be form independent.
I'd be happy to hide the button or letting the user click the button and preventing them from creating the sales order. It might be more user friendly doing the latter because if the button is hidden sales will be calling asking why the button is not there.
For clarity here is an image:
I am assuming that when you are talking about "the Sales Order button", you mean this one:
I'm not sure if this is the best user experience, or if you have NetSuite development resources available to you, but here is one option:
Create a new User Event script that is deployed to the Sales Order (and any other Transaction record you may want this prevention on). Using the BeforeLoad event, you can check if the Entity on the Transaction is in the Prospect stage. If they are, then the script will throw an error, preventing the creation of the Transaction. Code to accomplish this:
function onBeforeLoad(type) {
var entityId = nlapiGetFieldValue('entity');
if ((type != 'create') || !entityId) { return; }
if(nlapiLookupField('customer', nlapiGetFieldValue('entity'), 'stage') === 'PROSPECT') {
throw nlapiCreateError('INVALID_REQUEST', 'You cannot create a Sales Order from a Quote placed for a Prospect');
}
}
I tested this code in a TSTDRV account, and it works as expected. You might alternatively be able to build a workflow that does the same thing without requiring you to write code, but I did not attempt this.
By using a User Event script, this code will be form independent as well as entry point independent, meaning that this code will execute if the Sales Order is being created through the UI, through some other script, through a web services integration (depending on your web services configuration), or through a CSV import (depending on your CSV import configuration).
To hide the option:
If you're referring to the dropdown list, you can create a script for context view/edit to do the following:
setFieldAndLabelVisibility("nl13", false);
Otherwise, replace nl13 with the value of the table or td element shown when you inspect element on the desired Sales Order link/icon.
--The ID in the example above is the table, button or label ID shown when you inspect element
I have an online store where ~90% of what we sell is real, but ~10% is virtual. (software, training licenses)
Currently in our system all items are coded as "Inventory Item" because operationally on the back end there are benefits. (not sure exactly what)
Because they are inventory items, they show as out of stock and they require shipping, even though they are virtual.
Do I need to just add a custom field called "IsVirtual" and note which products are virtual explicitly, or is there a built-in mechanism for handling virtual products?
Best approach is to add those items in Netsuite as non inventory.
If you really want to keep them inventory and do not show shipping and out of stock, you got to write a SSP. Based on item attributes returned from search you can modify your code on front end to show/hide shipping and out of stock notice. Additionally when the order is created in your ssp add a logic to add dummy or a free shipping item and also a logic to add inventory.
You should make them non-inventory items for sale (unless you are reselling them).
However if you can't do that you do not need to create an SSP. All you need to do is alter the out-of-stock behavior for the item on the item setup page. You may need to tweak your item display template but you shouldn't need to unless you were already displaying stock levels on the buy page.