/
service.go
320 lines (267 loc) · 10.6 KB
/
service.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
package domain
import (
"context"
"math"
cartDomain "flamingo.me/flamingo-commerce/v3/cart/domain/cart"
"flamingo.me/flamingo-commerce/v3/cart/domain/decorator"
"flamingo.me/flamingo-commerce/v3/product/domain"
"flamingo.me/flamingo/v3/framework/flamingo"
"github.com/pkg/errors"
)
type (
// SourcingService describes the main port used by the sourcing logic.
SourcingService interface {
// AllocateItems returns Sources for the given item in the given cart
// e.g. use this during place order to know
// throws ErrInsufficientSourceQty if not enough stock is available for the amount of items in the cart
// throws ErrNoSourceAvailable if no source is available at all for one of the items
// throws ErrNeedMoreDetailsSourceCannotBeDetected if information on the cart (or delivery is missing)
AllocateItems(ctx context.Context, decoratedCart *decorator.DecoratedCart) (ItemAllocations, error)
// GetAvailableSources returns possible Sources for the product and the desired delivery.
// Optional the existing cart can be passed so that existing items in the cart can be evaluated also (e.g. deduct stock)
// e.g. use this before a product should be placed in the cart to know if and from where an item can be sourced
// throws ErrNeedMoreDetailsSourceCannotBeDetected
// throws ErrNoSourceAvailable if no source is available for the product and the given delivery
GetAvailableSources(ctx context.Context, product domain.BasicProduct, deliveryInfo *cartDomain.DeliveryInfo, decoratedCart *decorator.DecoratedCart) (AvailableSources, error)
}
// ItemID string alias
ItemID string
// ItemAllocations represents the allocated Qtys per itemID
ItemAllocations map[ItemID]ItemAllocation
// ItemAllocation info
ItemAllocation struct {
AllocatedQtys AllocatedQtys
Error error
}
// AllocatedQtys represents the allocated Qty per source
AllocatedQtys map[Source]int
// Source descriptor for a single location
Source struct {
// LocationCode identifies the warehouse or stock location
LocationCode string
// ExternalLocationCode identifies the source location in an external system
ExternalLocationCode string
}
// AvailableSources is the result value object containing the available Qty per Source
AvailableSources map[Source]int
// DefaultSourcingService provides a default implementation of the SourcingService interface.
// This default implementation is used unless a project overrides the interface binding.
DefaultSourcingService struct {
availableSourcesProvider AvailableSourcesProvider
stockProvider StockProvider
logger flamingo.Logger
}
// AvailableSourcesProvider interface for DefaultSourcingService
AvailableSourcesProvider interface {
GetPossibleSources(ctx context.Context, product domain.BasicProduct, deliveryInfo *cartDomain.DeliveryInfo) ([]Source, error)
}
// StockProvider interface for DefaultSourcingService
StockProvider interface {
GetStock(ctx context.Context, product domain.BasicProduct, source Source) (int, error)
}
)
var (
_ SourcingService = new(DefaultSourcingService)
// ErrInsufficientSourceQty - use to indicate that the requested qty exceeds the available qty
ErrInsufficientSourceQty = errors.New("Available Source Qty insufficient")
// ErrNoSourceAvailable - use to indicate that no source for item is available at all
ErrNoSourceAvailable = errors.New("No Available Source Qty")
// ErrNeedMoreDetailsSourceCannotBeDetected - use to indicate that informations are missing to determine a source
ErrNeedMoreDetailsSourceCannotBeDetected = errors.New("Source cannot be detected")
)
// Inject the dependencies
func (d *DefaultSourcingService) Inject(
logger flamingo.Logger,
dep *struct {
AvailableSourcesProvider AvailableSourcesProvider `inject:",optional"`
StockProvider StockProvider `inject:",optional"`
},
) *DefaultSourcingService {
d.logger = logger.WithField(flamingo.LogKeyModule, "sourcing").WithField(flamingo.LogKeyCategory, "DefaultSourcingService")
if dep != nil {
d.availableSourcesProvider = dep.AvailableSourcesProvider
d.stockProvider = dep.StockProvider
}
return d
}
// GetAvailableSources - see description in Interface
func (d *DefaultSourcingService) GetAvailableSources(ctx context.Context, product domain.BasicProduct, deliveryInfo *cartDomain.DeliveryInfo, decoratedCart *decorator.DecoratedCart) (AvailableSources, error) {
if err := d.checkConfiguration(); err != nil {
return nil, err
}
sources, err := d.availableSourcesProvider.GetPossibleSources(ctx, product, deliveryInfo)
if err != nil {
return nil, err
}
var lastStockError error
availableSources := AvailableSources{}
for _, source := range sources {
qty, err := d.stockProvider.GetStock(ctx, product, source)
if err != nil {
d.logger.Error(err)
lastStockError = err
continue
}
if qty > 0 {
availableSources[source] = qty
}
}
// if a cart is given we need to deduct the possible allocated items in the cart
if decoratedCart != nil {
allocatedSources, err := d.AllocateItems(ctx, decoratedCart)
if err != nil {
return nil, err
}
itemIdsWithProduct := getItemIdsWithProduct(decoratedCart, product)
for _, itemID := range itemIdsWithProduct {
availableSources = availableSources.Reduce(allocatedSources[itemID].AllocatedQtys)
}
}
if len(availableSources) == 0 {
if lastStockError != nil {
return availableSources, errors.Wrap(ErrNoSourceAvailable, lastStockError.Error())
}
return availableSources, ErrNoSourceAvailable
}
return availableSources, nil
}
// AllocateItems - see description in Interface
func (d *DefaultSourcingService) AllocateItems(ctx context.Context, decoratedCart *decorator.DecoratedCart) (ItemAllocations, error) {
if err := d.checkConfiguration(); err != nil {
return nil, err
}
if decoratedCart == nil {
return nil, errors.New("Cart not given")
}
// productSourcestock holds the available stock per product and source.
// During allocation the initial retrieved available stock is reduced according to used allocation
var productSourcestock = map[string]map[Source]int{}
if len(decoratedCart.DecoratedDeliveries) == 0 {
return nil, ErrNeedMoreDetailsSourceCannotBeDetected
}
resultItemAllocations := ItemAllocations{}
// overallError that will be returned
var overallError error
for _, delivery := range decoratedCart.DecoratedDeliveries {
for _, decoratedItem := range delivery.DecoratedItems {
var itemAllocation ItemAllocation
itemAllocation, productSourcestock = d.allocateItem(ctx, productSourcestock, decoratedItem, delivery.Delivery.DeliveryInfo)
resultItemAllocations[ItemID(decoratedItem.Item.ID)] = itemAllocation
}
}
return resultItemAllocations, overallError
}
func (d *DefaultSourcingService) checkConfiguration() error {
if d.availableSourcesProvider == nil {
d.logger.Error("no Source Provider bound")
return errors.New("no Source Provider bound")
}
if d.stockProvider == nil {
d.logger.Error("no Stock Provider bound")
return errors.New("no Stock Provider bound")
}
return nil
}
func getItemIdsWithProduct(dc *decorator.DecoratedCart, product domain.BasicProduct) []ItemID {
var result []ItemID
for _, di := range dc.GetAllDecoratedItems() {
if di.Product.GetIdentifier() == product.GetIdentifier() {
result = append(result, ItemID(di.Item.ID))
}
}
return result
}
// allocateItem returns the itemAllocation and the remaining stock for the given item.
// The passed productSourcestock is used - and the remaining productSourcestock is returned. In case a source is not yet given in productSourcestock it will be fetched
func (d *DefaultSourcingService) allocateItem(ctx context.Context, productSourcestock map[string]map[Source]int, decoratedItem decorator.DecoratedCartItem, deliveryInfo cartDomain.DeliveryInfo) (ItemAllocation, map[string]map[Source]int) {
var resultItemAllocation = ItemAllocation{
AllocatedQtys: make(AllocatedQtys),
}
// copy given known stock
remainingSourcestock := productSourcestock
productID := decoratedItem.Product.GetIdentifier()
if productID == "" {
return ItemAllocation{
Error: errors.New("product id missing"),
}, remainingSourcestock
}
sources, err := d.availableSourcesProvider.GetPossibleSources(ctx, decoratedItem.Product, &deliveryInfo)
if err != nil {
return ItemAllocation{
Error: err,
}, remainingSourcestock
}
if len(sources) == 0 {
return ItemAllocation{
Error: ErrNoSourceAvailable,
}, remainingSourcestock
}
qtyToAllocate := decoratedItem.Item.Qty
allocatedQty := 0
if _, exists := productSourcestock[productID]; !exists {
productSourcestock[productID] = make(map[Source]int)
}
for _, source := range sources {
// if we have no stock given for source and productid we fetch it initially
if _, exists := remainingSourcestock[productID][source]; !exists {
sourceStock, err := d.stockProvider.GetStock(ctx, decoratedItem.Product, source)
if err != nil {
d.logger.Error(err)
continue
}
remainingSourcestock[productID][source] = sourceStock
}
if remainingSourcestock[productID][source] == 0 {
continue
}
if allocatedQty < qtyToAllocate {
// stock to write to result allocation is the lowest of either :
// - the remaining qty that is to be allocated
// OR
// - the existing sourceStock that is then used completely
stockToAllocate := min(qtyToAllocate-allocatedQty, productSourcestock[productID][source])
resultItemAllocation.AllocatedQtys[source] = stockToAllocate
// increment allocatedQty by allocated Stock
allocatedQty = allocatedQty + stockToAllocate
// decrement remaining productSourceStock accordingly as its not happening by itself
remainingSourcestock[productID][source] = remainingSourcestock[productID][source] - stockToAllocate
}
}
if allocatedQty < qtyToAllocate {
resultItemAllocation.Error = ErrInsufficientSourceQty
}
return resultItemAllocation, remainingSourcestock
}
// QtySum returns the sum of all sourced items
func (s AvailableSources) QtySum() int {
qty := 0
for _, sqty := range s {
if sqty == math.MaxInt64 {
return sqty
}
qty = qty + sqty
}
return qty
}
// Reduce returns new AvailableSources reduced by the given AvailableSources
func (s AvailableSources) Reduce(reducedBy AllocatedQtys) AvailableSources {
newAvailableSources := make(AvailableSources)
for source, availableQty := range s {
if allocated, ok := reducedBy[source]; ok {
newQty := availableQty - allocated
if newQty > 0 {
newAvailableSources[source] = newQty
}
} else {
newAvailableSources[source] = availableQty
}
}
return newAvailableSources
}
// min returns minimum of 2 ints
func min(a int, b int) int {
if a < b {
return a
}
return b
}