Compare commits
7
Commits
fb07b0a642
...
b5e57ce659
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b5e57ce659 | ||
|
|
974fd4e523 | ||
|
|
761179920b | ||
|
|
8a302f8529 | ||
|
|
1d86789b65 | ||
|
|
6862c22df4 | ||
|
|
74cdd90b68 |
@@ -16,3 +16,4 @@ Per-repo topic registry for this plugin, per the **per-repo extension convention
|
||||
| `ORDD` | ORDER-DRAFT | AI-assisted intake channel: inbound free-text messages (Viber/email/dictation) parsed into order drafts, admin review/approval, then PreOrder creation that feeds the existing PRE-ORDER conversion. Covers parsing, product/partner resolution, learning mappings, the draft lifecycle and the approval entry point. | `docs/ORDERDRAFT/` |
|
||||
| `MARGIN`| ÁRRÉS | Beszerzési önköltség és árrés: a rendelés-tételek ↔ beszerzések (költség-rétegek) hozzárendelése, a fuvar- és egyéb költségek tárolása és szétosztása, deviza- és áfakezelés az árrés szempontjából, valamint a kimutatás dokumentum / kamion / hónap bontásban. | `docs/MARGIN/` |
|
||||
| `PMDATA`| TERMÉK-TÖRZSADAT | A termék mérési/logisztikai törzsadata (`IsMeasurable`, `Tare`, `AverageWeight`, `AverageWeightTreshold`, `CratesPerPallet`): tárolása, írási útjai, validálása és tömeges szerkesztése. A készlet-vezérelt értékek (`NetWeight`, `IncomingQuantity`) NEM ide tartoznak — azok a `STOCK` és a `PREO` topiké. | `docs/PMDATA/` |
|
||||
| `NOTIF` | ÉRTESÍTÉS | Vevői e-mail értesítők: a plugin saját `MessageTemplate`-jei, a nop token-provider kiváltása és a saját tokenek, a `MeasuringStatus`-átmenetekhez kötött kiváltás, valamint az ismétlés elleni jelölés. A nop natív értesítői NEM ide tartoznak. | `docs/NOTIFICATION/` |
|
||||
|
||||
@@ -19,7 +19,7 @@ using Nop.Services.Catalog;
|
||||
|
||||
namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers;
|
||||
|
||||
public class CustomOrderSignalREndpoint(FruitBankDbContext ctx, SignalRSendToClientService sendToClient, IPriceCalculationService customPriceCalculationService,IEventPublisher eventPublisher, IWorkContext workContext, IEnumerable<IAcLogWriterBase> logWriters)
|
||||
public class CustomOrderSignalREndpoint(FruitBankDbContext ctx, SignalRSendToClientService sendToClient, IPriceCalculationService customPriceCalculationService,IEventPublisher eventPublisher, IWorkContext workContext, FruitBankNotificationService fruitBankNotificationService, IEnumerable<IAcLogWriterBase> logWriters)
|
||||
: ICustomOrderSignalREndpointServer
|
||||
{
|
||||
// PROD-on 15 napos adat-ablak; DEV-en 30 nap (a PROD-ból másolt adaton már bőven van mit listázni).
|
||||
@@ -146,7 +146,29 @@ public class CustomOrderSignalREndpoint(FruitBankDbContext ctx, SignalRSendToCli
|
||||
_logger.Detail($"StartMeasuring invoked; orderId: {orderId}; userId: {userId}");
|
||||
|
||||
if (!await ctx.StartMeasuringSafeAsync(orderId, userId)) return null;
|
||||
return await ctx.OrderDtos.GetByIdAsync(orderId, true);
|
||||
|
||||
var orderDto = await ctx.OrderDtos.GetByIdAsync(orderId, true);
|
||||
|
||||
// A mérésindítás csak a MeasurementOwnerId GenericAttribute-ot írja, Order-sort nem — ezért nem keletkezik
|
||||
// `EntityUpdatedEvent<Order>`, amire a Services/EventConsumer figyel, és a „Started" értesítőnek nincs
|
||||
// kiváltója. A hívás a tranzakción KÍVÜL áll: ha a mérésindítás visszagördül, ne menjen ki levél.
|
||||
await SendOrderStartedNotificationAsync(orderDto);
|
||||
|
||||
return orderDto;
|
||||
}
|
||||
|
||||
/// <summary>Sends the "order started" customer notification for a just-started measurement.</summary>
|
||||
private async Task SendOrderStartedNotificationAsync(OrderDto orderDto)
|
||||
{
|
||||
if (orderDto == null) return;
|
||||
|
||||
var order = ctx.Orders.GetById(orderDto.Id);
|
||||
if (order == null) return;
|
||||
|
||||
// A StartMeasuringSafeAsync akkor is sikert jelez, ha a mérést már korábban elindították (a
|
||||
// StartMeasuringAsync ilyenkor változatlanul visszaadja a rendelést) — az ismétlés ellen a szolgáltatás
|
||||
// jelölése véd, nem a hívás eredménye.
|
||||
await fruitBankNotificationService.SendOrderStartedCustomerNotificationOnceAsync(order, orderDto.IsMeasurable);
|
||||
}
|
||||
|
||||
[SignalR(SignalRTags.SetOrderStatusToComplete, SignalRTags.SendOrderChanged, SendToClientType.Others)]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#nullable enable
|
||||
#nullable enable
|
||||
using System.Threading.Tasks;
|
||||
using AyCode.Core.Loggers;
|
||||
using FruitBank.Common.Dtos;
|
||||
@@ -26,6 +26,7 @@ public class StockTakingDbContext : MgDbContextBase,
|
||||
IStockTakingItemPalletDbSet<StockTakingItemPalletDbTable>
|
||||
{
|
||||
private FruitBankDbContext _fruitBankDbContext;
|
||||
private readonly CostAttributionService _costAttributionService;
|
||||
public ProductDtoDbTable ProductDtos { get; set; }
|
||||
public OrderItemDtoDbTable OrderItemDtos { get; set; }
|
||||
|
||||
@@ -44,6 +45,7 @@ public class StockTakingDbContext : MgDbContextBase,
|
||||
protected readonly IEventPublisher _eventPublisher;
|
||||
|
||||
public StockTakingDbContext(INopDataProvider dataProvider, ILockService lockService, IStoreContext storeContext, FruitBankDbContext fruitBankDbContext,
|
||||
CostAttributionService costAttributionService,
|
||||
ProductDtoDbTable productDtoDbTable,
|
||||
OrderItemDtoDbTable orderItemDtoDbTable,
|
||||
StockQuantityHistoryDtoDbTable stockQuantityHistoryDtos,
|
||||
@@ -66,6 +68,7 @@ public class StockTakingDbContext : MgDbContextBase,
|
||||
_staticCacheManager = staticCacheManager;
|
||||
|
||||
_fruitBankDbContext = fruitBankDbContext;
|
||||
_costAttributionService = costAttributionService;
|
||||
|
||||
ProductDtos = productDtoDbTable;
|
||||
OrderItemDtos = orderItemDtoDbTable;
|
||||
@@ -100,6 +103,11 @@ public class StockTakingDbContext : MgDbContextBase,
|
||||
$"Leltár által módosítva! stockTakingId: #{stockTaking.Id}, stockTakingItemId: #{stockTakingItem.Id}",
|
||||
stockTakingItem.NetWeightDiff);
|
||||
|
||||
// A leltári hiány ugyanúgy fogyaszt beszerzési réteget, mint az eladás (ADR 0002-R7Q2) — enélkül a
|
||||
// leírt áru bent maradna a rétegben, és a riport készletnek mutatná. Csak a hiány számít; a többlethez
|
||||
// nem tartozik beszerzés, azt a szolgáltatás kihagyja.
|
||||
await _costAttributionService.AttributeStockTakingLossAsync(stockTakingItem, stockTaking.Creator);
|
||||
|
||||
Logger.Debug($"CloseStockTaking.UpdateStockQuantityAndWeightAsync; {stockTakingItem.Product}; qntDiff: {stockTakingItem.QuantityDiff} db; netWeightDiff: {stockTakingItem.NetWeightDiff} kg");
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ using Nop.Core.Domain.Tax;
|
||||
using Nop.Core.Domain.Vendors;
|
||||
using Nop.Core.Events;
|
||||
using Nop.Plugin.Misc.FruitBankPlugin.Domains.DataLayer;
|
||||
using Nop.Plugin.Misc.FruitBankPlugin.Services;
|
||||
using Nop.Services.Attributes;
|
||||
using Nop.Services.Blogs;
|
||||
using Nop.Services.Catalog;
|
||||
@@ -39,6 +40,15 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Infrastructure
|
||||
{
|
||||
public class FruitBankMessageTokenProvider : MessageTokenProvider
|
||||
{
|
||||
/// <summary>The order total token — the FruitBank-calculated amount stored on the order.</summary>
|
||||
public const string ORDER_TOTAL_TOKEN = "Order.OrderTotal";
|
||||
|
||||
/// <summary>The measurable-order note token — its value is HTML, supplied by the notification service.</summary>
|
||||
public const string ORDER_MEASURABLE_NOTE_TOKEN = "Order.MeasurableNote";
|
||||
|
||||
/// <summary>The customer company token — a real column, but nop ships no token for it.</summary>
|
||||
public const string CUSTOMER_COMPANY_TOKEN = "Customer.Company";
|
||||
|
||||
private readonly IOrderService _orderService;
|
||||
private readonly IPriceFormatter _priceFormatter;
|
||||
private readonly ICurrencyService _currencyService;
|
||||
@@ -145,12 +155,69 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Infrastructure
|
||||
tokens.Remove(existing);
|
||||
|
||||
tokens.Add(new Token("Order.Product(s)", await BuildCustomProductTableAsync(order, languageId), true));
|
||||
|
||||
// A nopnak NINCS `Order.OrderTotal` tokenje — a végösszeget csak a termék-tábla lábában jeleníti meg,
|
||||
// és ott is az árfolyammal átszámított értéket. Nekünk a saját, mérés utáni szám kell: a
|
||||
// CustomPriceCalculationService.CheckAndUpdateOrderTotalPrice írja az `Order.OrderTotal` oszlopba a
|
||||
// tételek bruttó (kedvezménnyel csökkentett) árainak összegét — a token ezt az eltárolt értéket adja.
|
||||
tokens.Add(new Token(ORDER_TOTAL_TOKEN, await FormatOrderTotalAsync(order, languageId)));
|
||||
}
|
||||
|
||||
/// <summary>Adds the customer's company to the customer tokens.</summary>
|
||||
public override async Task AddCustomerTokensAsync(IList<Token> tokens, Customer customer)
|
||||
{
|
||||
await base.AddCustomerTokensAsync(tokens, customer);
|
||||
|
||||
// A `Company` valódi oszlop a Customer entitáson, a nop mégsem ad rá tokent — cégnevet csak a rendelés
|
||||
// címeiből kínál (`Order.BillingCompany`). B2B-ben a vevő cége a rendeléstől független törzsadat, ezért
|
||||
// saját tokent kap. A bázis `int`-es túlterhelése ide delegál, tehát mindkét hívási út fedve van.
|
||||
tokens.Add(new Token(CUSTOMER_COMPANY_TOKEN, customer.Company));
|
||||
}
|
||||
|
||||
/// <summary>Maps the FruitBank message templates to their token groups.</summary>
|
||||
public override IEnumerable<string> GetTokenGroups(MessageTemplate messageTemplate)
|
||||
{
|
||||
// A bázis a nop saját `MessageTemplateSystemNames` konstansaira switch-el, és ismeretlen sablonnévre
|
||||
// ÜRES csoportlistát ad. A `GetListOfAllowedTokensAsync` szűrője az üres listát nem `null`-ként kezeli,
|
||||
// ezért maradt üresen az admin „Engedélyezett tokenek" doboza a két FruitBank-sablonnál.
|
||||
if (messageTemplate.Name == FruitBankNotificationService.ORDER_STARTED_TEMPLATE_NAME ||
|
||||
messageTemplate.Name == FruitBankNotificationService.ORDER_AUDITED_TEMPLATE_NAME)
|
||||
return [TokenGroupNames.StoreTokens, TokenGroupNames.OrderTokens, TokenGroupNames.CustomerTokens];
|
||||
|
||||
return base.GetTokenGroups(messageTemplate);
|
||||
}
|
||||
|
||||
/// <summary>Adds the FruitBank-specific order tokens to the admin's allowed-token list.</summary>
|
||||
public override async Task<IEnumerable<string>> GetListOfAllowedTokensAsync(IEnumerable<string> tokenGroups = null)
|
||||
{
|
||||
var allowedTokens = (await base.GetListOfAllowedTokensAsync(tokenGroups)).ToList();
|
||||
|
||||
// A bázis a privát `AllowedTokens` szótárból dolgozik — a saját tokenjeink csak itt kerülhetnek a listába.
|
||||
if (tokenGroups == null || tokenGroups.Contains(TokenGroupNames.OrderTokens))
|
||||
{
|
||||
allowedTokens.Add($"%{ORDER_TOTAL_TOKEN}%");
|
||||
allowedTokens.Add($"%{ORDER_MEASURABLE_NOTE_TOKEN}%");
|
||||
}
|
||||
|
||||
if (tokenGroups == null || tokenGroups.Contains(TokenGroupNames.CustomerTokens))
|
||||
allowedTokens.Add($"%{CUSTOMER_COMPANY_TOKEN}%");
|
||||
|
||||
return allowedTokens.Distinct();
|
||||
}
|
||||
|
||||
/// <summary>The order's display currency — the customer's, falling back to the primary store currency.</summary>
|
||||
private async Task<Currency> GetOrderCurrencyAsync(Order order)
|
||||
=> await _currencyService.GetCurrencyByCodeAsync(order.CustomerCurrencyCode)
|
||||
?? await _currencyService.GetCurrencyByIdAsync(_currencySettings.PrimaryStoreCurrencyId);
|
||||
|
||||
/// <summary>The stored order total, formatted — the single source for both the token and the table footer.</summary>
|
||||
private async Task<string> FormatOrderTotalAsync(Order order, int languageId)
|
||||
=> await _priceFormatter.FormatPriceAsync(
|
||||
order.OrderTotal, true, await GetOrderCurrencyAsync(order), languageId, true);
|
||||
|
||||
private async Task<string> BuildCustomProductTableAsync(Order order, int languageId)
|
||||
{
|
||||
var currency = await _currencyService.GetCurrencyByCodeAsync(order.CustomerCurrencyCode)
|
||||
?? await _currencyService.GetCurrencyByIdAsync(_currencySettings.PrimaryStoreCurrencyId);
|
||||
var currency = await GetOrderCurrencyAsync(order);
|
||||
|
||||
var items = await _orderService.GetOrderItemsAsync(order.Id);
|
||||
var itemDtos = await _dbContext.OrderItemDtos.GetAllByOrderId(order.Id).ToListAsync();
|
||||
@@ -206,8 +273,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Infrastructure
|
||||
}
|
||||
}
|
||||
|
||||
var orderTotal = await _priceFormatter.FormatPriceAsync(
|
||||
order.OrderTotal, true, currency, languageId, true);
|
||||
var orderTotal = await FormatOrderTotalAsync(order, languageId);
|
||||
|
||||
if(itemDtos.Any(i => i.IsMeasurable))
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using AyCode.Core.Loggers;
|
||||
using FruitBank.Common.Dtos;
|
||||
using FruitBank.Common.Entities;
|
||||
using LinqToDB;
|
||||
using Mango.Nop.Core.Loggers;
|
||||
using Nop.Plugin.Misc.FruitBankPlugin.Domains.DataLayer;
|
||||
@@ -32,27 +33,55 @@ public class CostAttributionService(
|
||||
|
||||
/// <summary>Egy rendelés-tétel áruját szétosztja a beszerzési rétegek között; a hívó tranzakciójában fut.</summary>
|
||||
/// <returns>A létrehozott fogyasztás-sorok száma; 0, ha nem volt mit kiosztani.</returns>
|
||||
public async Task<int> AttributeOrderItemAsync(OrderItemDto orderItemDto, int? creatorId = null)
|
||||
public Task<int> AttributeOrderItemAsync(OrderItemDto orderItemDto, int? creatorId = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(orderItemDto);
|
||||
|
||||
// Idempotencia: a lezárás nem futhat kétszer ugyanarra a rendelésre, de ha mégis, ne duplázzunk.
|
||||
var netWeight = double.Round(orderItemDto.NetWeight, 1);
|
||||
|
||||
return AttributeAsync(CostConsumerType.OrderItem, orderItemDto.Id, orderItemDto.ProductId,
|
||||
orderItemDto.IsMeasurable, orderItemDto.Quantity, netWeight, creatorId);
|
||||
}
|
||||
|
||||
/// <summary>Egy leltári HIÁNY áruját szétosztja a beszerzési rétegek között; a hívó tranzakciójában fut.</summary>
|
||||
/// <remarks>
|
||||
/// Csak a **hiány** fogyaszt réteget: a leltári többlet (pozitív eltérés) nem, mert ahhoz nem tartozik
|
||||
/// beszerzés — azt a modell nem tudja hova kötni, és negatív fogyasztásként ábrázolni félrevezető lenne.
|
||||
/// A többlet így a rétegekben marad, és a következő eladás fogyasztja el.
|
||||
///
|
||||
/// Enélkül a leírt áru bent maradna a rétegben, és a riport `Maradt` oszlopa készletnek mutatná.
|
||||
/// </remarks>
|
||||
public Task<int> AttributeStockTakingLossAsync(StockTakingItem stockTakingItem, int? creatorId = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(stockTakingItem);
|
||||
|
||||
// A hiány pozitív számként: a diff negatív, ha kevesebb van, mint amennyi a nyilvántartás szerint lenne.
|
||||
var lostQuantity = stockTakingItem.QuantityDiff < 0 ? -stockTakingItem.QuantityDiff : 0;
|
||||
var lostNetWeight = stockTakingItem.NetWeightDiff < 0 ? double.Round(-stockTakingItem.NetWeightDiff, 1) : 0d;
|
||||
|
||||
return AttributeAsync(CostConsumerType.StockTakingItem, stockTakingItem.Id, stockTakingItem.ProductId,
|
||||
stockTakingItem.IsMeasurable, lostQuantity, lostNetWeight, creatorId);
|
||||
}
|
||||
|
||||
/// <summary>A FIFO-kiosztás közös magja — a fogyasztó fajtájától független.</summary>
|
||||
private async Task<int> AttributeAsync(CostConsumerType consumerType, int consumerId, int productId,
|
||||
bool isMeasurable, int quantity, double netWeight, int? creatorId)
|
||||
{
|
||||
// Idempotencia: a lezárás nem futhat kétszer ugyanarra a fogyasztóra, de ha mégis, ne duplázzunk.
|
||||
// A hívás minősítve: az `AnyAsync()` nevet a LinqToDB és a System.Linq aszinkron kiterjesztése is viszi.
|
||||
var alreadyDone = await LinqToDB.AsyncExtensions.AnyAsync(
|
||||
costConsumptions.GetAllByConsumer(CostConsumerType.OrderItem, orderItemDto.Id));
|
||||
costConsumptions.GetAllByConsumer(consumerType, consumerId));
|
||||
|
||||
if (alreadyDone) return 0;
|
||||
|
||||
var isMeasurable = orderItemDto.IsMeasurable;
|
||||
|
||||
// A vezérlő dimenzió: mérendőnél a mért nettó súly, nem mérendőnél a rekeszszám.
|
||||
var needed = isMeasurable ? double.Round(orderItemDto.NetWeight, 1) : orderItemDto.Quantity;
|
||||
// A vezérlő dimenzió: mérendőnél a nettó súly, nem mérendőnél a rekeszszám.
|
||||
var needed = isMeasurable ? netWeight : quantity;
|
||||
if (needed <= 0) return 0;
|
||||
|
||||
var layers = await LoadLayersAsync(orderItemDto.ProductId, isMeasurable);
|
||||
var layers = await LoadLayersAsync(productId, isMeasurable);
|
||||
|
||||
// A tétel összesenjei a MÁSIK dimenzió arányosításához; a vezérlő dimenzió mindig egzakt.
|
||||
var totals = new ItemTotals(needed, orderItemDto.Quantity, double.Round(orderItemDto.NetWeight, 1));
|
||||
var totals = new ItemTotals(needed, quantity, netWeight);
|
||||
|
||||
var created = 0;
|
||||
|
||||
@@ -63,7 +92,7 @@ public class CostAttributionService(
|
||||
|
||||
var take = Math.Min(layer.Remaining, needed);
|
||||
|
||||
await InsertConsumptionAsync(layer.ShippingItemId, orderItemDto.Id, isMeasurable, take, totals, false, creatorId);
|
||||
await InsertConsumptionAsync(layer.ShippingItemId, consumerType, consumerId, isMeasurable, take, totals, false, creatorId);
|
||||
|
||||
needed -= take;
|
||||
created++;
|
||||
@@ -72,11 +101,11 @@ public class CostAttributionService(
|
||||
// Ami nem talált réteget: ismeretlen forrású. A riport külön során jelenik meg.
|
||||
if (needed > (isMeasurable ? WeightEpsilon : 0))
|
||||
{
|
||||
await InsertConsumptionAsync(null, orderItemDto.Id, isMeasurable, needed, totals, false, creatorId);
|
||||
await InsertConsumptionAsync(null, consumerType, consumerId, isMeasurable, needed, totals, false, creatorId);
|
||||
created++;
|
||||
|
||||
_logger.Warning($"CostAttributionService: ismeretlen forrású fogyasztás. " +
|
||||
$"OrderItem.Id: {orderItemDto.Id}, Product.Id: {orderItemDto.ProductId}, " +
|
||||
$"{consumerType}.Id: {consumerId}, Product.Id: {productId}, " +
|
||||
$"maradék: {needed:0.###} {(isMeasurable ? "kg" : "rekesz")}");
|
||||
}
|
||||
|
||||
@@ -129,15 +158,16 @@ public class CostAttributionService(
|
||||
/// egzaktul adódik (FBANKAPP ADR 0001), mérendőnél a rekeszszám a tétel saját összesenjéből. Egyetlen rétegnél
|
||||
/// — a tipikus eset — az arány 1, tehát mindkét érték pontos; bontásnál a vezérlő dimenzió marad a hiteles.
|
||||
/// </remarks>
|
||||
private async Task InsertConsumptionAsync(int? shippingItemId, int orderItemId, bool isMeasurable, double amount, ItemTotals totals, bool isManual, int? creatorId)
|
||||
private async Task InsertConsumptionAsync(int? shippingItemId, CostConsumerType consumerType, int consumerId,
|
||||
bool isMeasurable, double amount, ItemTotals totals, bool isManual, int? creatorId)
|
||||
{
|
||||
var ratio = totals.Driving > 0 ? amount / totals.Driving : 0d;
|
||||
|
||||
var consumption = new CostConsumption
|
||||
{
|
||||
ShippingItemId = shippingItemId,
|
||||
ConsumerType = CostConsumerType.OrderItem,
|
||||
ConsumerId = orderItemId,
|
||||
ConsumerType = consumerType,
|
||||
ConsumerId = consumerId,
|
||||
Quantity = isMeasurable ? (int)Math.Round(totals.Quantity * ratio) : (int)Math.Round(amount),
|
||||
NetWeight = isMeasurable ? double.Round(amount, 1) : double.Round(totals.NetWeight * ratio, 1),
|
||||
IsManual = isManual,
|
||||
|
||||
@@ -138,38 +138,16 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services
|
||||
var orderDto = await _dbContext.OrderDtos.GetByIdAsync(eventMessage.Entity.Id, true);
|
||||
if (orderDto == null) return;
|
||||
|
||||
// Az egyszeri küldés (jelölés-ellenőrzés → küldés → jelölés CSAK sikeres sorbaállítás után) a
|
||||
// FruitBankNotificationService-ben lakik: a `Started` ágat a CustomOrderSignalREndpoint is kiváltja,
|
||||
// és a két hívónak ugyanazt az idempotenciát kell látnia.
|
||||
if (orderDto.MeasuringStatus == MeasuringStatus.Audited)
|
||||
{
|
||||
var alreadySent = await _fruitBankAttributeService
|
||||
.GetGenericAttributeValueAsync<Order, bool>(eventMessage.Entity.Id, "OrderAuditedNotificationSent");
|
||||
await _fruitBankNotificationService
|
||||
.SendOrderAuditedCustomerNotificationOnceAsync(eventMessage.Entity, orderDto.IsMeasurable);
|
||||
|
||||
if (!alreadySent)
|
||||
{
|
||||
await _fruitBankNotificationService
|
||||
.SendOrderAuditedCustomerNotificationAsync(eventMessage.Entity, orderDto.IsMeasurable);
|
||||
|
||||
await _fruitBankAttributeService
|
||||
.InsertOrUpdateGenericAttributeAsync<Order, bool>(
|
||||
eventMessage.Entity.Id, "OrderAuditedNotificationSent", true);
|
||||
}
|
||||
}
|
||||
else if (orderDto.MeasuringStatus == MeasuringStatus.Started)
|
||||
{
|
||||
var alreadySent = await _fruitBankAttributeService
|
||||
.GetGenericAttributeValueAsync<Order, bool>(eventMessage.Entity.Id, "OrderStartedNotificationSent");
|
||||
|
||||
if (!alreadySent)
|
||||
{
|
||||
await _fruitBankNotificationService
|
||||
.SendOrderStartedCustomerNotificationAsync(eventMessage.Entity, orderDto.IsMeasurable);
|
||||
|
||||
await _fruitBankAttributeService
|
||||
.InsertOrUpdateGenericAttributeAsync<Order, bool>(
|
||||
eventMessage.Entity.Id, "OrderStartedNotificationSent", true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
await _fruitBankNotificationService
|
||||
.SendOrderStartedCustomerNotificationOnceAsync(eventMessage.Entity, orderDto.IsMeasurable);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ using Nop.Core.Domain.Common;
|
||||
using Nop.Core.Domain.Messages;
|
||||
using Nop.Core.Domain.Orders;
|
||||
using Nop.Core.Events;
|
||||
using Nop.Plugin.Misc.FruitBankPlugin.Infrastructure;
|
||||
using Nop.Services.Affiliates;
|
||||
using Nop.Services.Catalog;
|
||||
using Nop.Services.Common;
|
||||
@@ -37,6 +38,7 @@ public class FruitBankNotificationService(
|
||||
// A nop natív naplózója: a bejegyzések az Admin → Rendszer → Napló alatt megjelennek. Enélkül egy elmaradt
|
||||
// vevői értesítés nyomtalan maradna — élesben pontosan ez rejtett el egy üres törzsű sablont.
|
||||
Nop.Services.Logging.ILogger logger,
|
||||
FruitBankAttributeService fruitBankAttributeService,
|
||||
MessagesSettings messagesSettings) : WorkflowMessageService(commonSettings,
|
||||
emailAccountSettings,
|
||||
addressService,
|
||||
@@ -59,6 +61,12 @@ public class FruitBankNotificationService(
|
||||
public const string ORDER_AUDITED_TEMPLATE_NAME = "FruitBank.OrderAudited.CustomerNotification";
|
||||
public const string ORDER_STARTED_TEMPLATE_NAME = "FruitBank.OrderStarted.CustomerNotification";
|
||||
|
||||
/// <summary>GenericAttribute key guarding the "order started" notification against repeats.</summary>
|
||||
public const string ORDER_STARTED_SENT_KEY = "OrderStartedNotificationSent";
|
||||
|
||||
/// <summary>GenericAttribute key guarding the "order audited" notification against repeats.</summary>
|
||||
public const string ORDER_AUDITED_SENT_KEY = "OrderAuditedNotificationSent";
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -162,6 +170,32 @@ public class FruitBankNotificationService(
|
||||
return await SendNotificationAsync(ORDER_AUDITED_TEMPLATE_NAME, order, measurableNote);
|
||||
}
|
||||
|
||||
/// <summary>Sends the "order started" notification unless it already went out for this order.</summary>
|
||||
public Task<bool> SendOrderStartedCustomerNotificationOnceAsync(Order order, bool isMeasurable)
|
||||
=> SendOnceAsync(order, ORDER_STARTED_SENT_KEY, () => SendOrderStartedCustomerNotificationAsync(order, isMeasurable));
|
||||
|
||||
/// <summary>Sends the "order audited" notification unless it already went out for this order.</summary>
|
||||
public Task<bool> SendOrderAuditedCustomerNotificationOnceAsync(Order order, bool isMeasurable)
|
||||
=> SendOnceAsync(order, ORDER_AUDITED_SENT_KEY, () => SendOrderAuditedCustomerNotificationAsync(order, isMeasurable));
|
||||
|
||||
// ── idempotencia ────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Runs the send once per order, keyed by a GenericAttribute flag.</summary>
|
||||
private async Task<bool> SendOnceAsync(Order order, string sentKey, Func<Task<int>> sendAsync)
|
||||
{
|
||||
if (await fruitBankAttributeService.GetGenericAttributeValueAsync<Order, bool>(order.Id, sentKey)) return false;
|
||||
|
||||
var queuedEmailId = await sendAsync();
|
||||
|
||||
// A jelölés CSAK sikeres sorbaállítás után íródik. A küldés minden hibaágon 0-t ad vissza (hiányzó vagy
|
||||
// inaktív sablon, nincs használható e-mail fiók, nincs vevő), és egy ilyenkor is kiírt jelölés VÉGLEGESEN
|
||||
// elnémítaná a rendelést: a két értesítőnek nincs második kiváltója, amire újrapróbálkozhatna.
|
||||
if (queuedEmailId <= 0) return false;
|
||||
|
||||
await fruitBankAttributeService.InsertOrUpdateGenericAttributeAsync<Order, bool>(order.Id, sentKey, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ── shared core ─────────────────────────────────────────────────────────
|
||||
|
||||
private async Task<int> SendNotificationAsync(string templateName, Order order, string measurableNote)
|
||||
@@ -215,7 +249,7 @@ public class FruitBankNotificationService(
|
||||
|
||||
await messageTokenProvider.AddCustomerTokensAsync(tokens, customer);
|
||||
|
||||
tokens.Add(new Token("Order.MeasurableNote", measurableNote, true));
|
||||
tokens.Add(new Token(FruitBankMessageTokenProvider.ORDER_MEASURABLE_NOTE_TOKEN, measurableNote, true));
|
||||
|
||||
int addressId = 0;
|
||||
|
||||
|
||||
@@ -66,6 +66,11 @@ public class MarginCalculationService(
|
||||
var (freightByItem, truckSharesByItem) = await CalculateFreightAndTruckSharesAsync(layerIds);
|
||||
var extraByItem = await CalculateDocumentExtraShareByItemAsync(layers.Select(l => l.ShippingDocumentId).Distinct().ToList());
|
||||
|
||||
// A réteg TELJES fogyasztása — minden időszak és minden fogyasztó-fajta, tehát az eladás mellett a
|
||||
// leltári hiány is. A `Maradt` oszlop csak ebből lehet helyes: az időszak eladásából levezetve azt
|
||||
// mutatná maradéknak, ami korábbi hónapokban már elfogyott vagy leírásra került.
|
||||
var consumedTotalByLayer = await LoadTotalConsumptionAsync(layerIds, layers.ToDictionary(l => l.Id, l => l.IsMeasurable));
|
||||
|
||||
// ── Soronkénti összeállítás ─────────────────────────────────────────────
|
||||
var rows = new List<MarginRow>();
|
||||
|
||||
@@ -102,9 +107,11 @@ public class MarginCalculationService(
|
||||
var freightShare = freightByItem.GetValueOrDefault(layer.Id, 0m) * soldRatio;
|
||||
var extraShare = extraByItem.GetValueOrDefault(layer.Id, 0m) * soldRatio;
|
||||
|
||||
var consumedTotal = consumedTotalByLayer.GetValueOrDefault(layer.Id, 0d);
|
||||
|
||||
rows.Add(new MarginRow(
|
||||
layer.Id, layer.ShippingDocumentId, layer.ProductId, layer.Name, isMeasurable,
|
||||
received, sold, received - sold,
|
||||
received, sold, received - consumedTotal,
|
||||
goodsCost, freightShare, extraShare,
|
||||
revenue, revenue - goodsCost - freightShare - extraShare, false,
|
||||
truckSharesByItem.GetValueOrDefault(layer.Id, []), group.Key.Period,
|
||||
@@ -184,6 +191,29 @@ public class MarginCalculationService(
|
||||
return (result, truckSharesByItem);
|
||||
}
|
||||
|
||||
/// <summary>Rétegenként a TELJES eddigi fogyasztás a vezérlő dimenzióban — minden időszak, minden fajta.</summary>
|
||||
/// <remarks>
|
||||
/// Az eladás és a leltári hiány egyaránt fogyaszt, ezért nincs fogyasztó-fajta szűrés. A riport `Maradt`
|
||||
/// oszlopa ebből áll elő; az időszakra szűrt eladás erre nem alkalmas, mert a korábbi hónapokban elfogyott
|
||||
/// vagy leírt mennyiséget készletnek mutatná.
|
||||
/// </remarks>
|
||||
private async Task<Dictionary<int, double>> LoadTotalConsumptionAsync(List<int> layerIds, Dictionary<int, bool> measurableByLayer)
|
||||
{
|
||||
if (layerIds.Count == 0) return [];
|
||||
|
||||
var rows = await costConsumptions
|
||||
.GetAllByShippingItemIds(layerIds)
|
||||
.Select(c => new { c.ShippingItemId, c.Quantity, c.NetWeight })
|
||||
.ToListAsync();
|
||||
|
||||
return rows
|
||||
.Where(r => r.ShippingItemId.HasValue)
|
||||
.GroupBy(r => r.ShippingItemId!.Value)
|
||||
.ToDictionary(
|
||||
g => g.Key,
|
||||
g => measurableByLayer.GetValueOrDefault(g.Key) ? g.Sum(x => x.NetWeight) : g.Sum(x => x.Quantity));
|
||||
}
|
||||
|
||||
/// <summary>A szállítólevél-szintű költségek tételre eső része, ugyanazzal a raklap-kulccsal.</summary>
|
||||
private async Task<Dictionary<int, decimal>> CalculateDocumentExtraShareByItemAsync(List<int> documentIds)
|
||||
{
|
||||
|
||||
@@ -49,9 +49,13 @@ lépések és az állapotuk.
|
||||
raklapokra osztva), extra-hányad, árrés — mind lekérdezésből, tárolt pénzügyi érték nélkül.
|
||||
- [x] **Admin riport** dokumentum- és kamion-bontásban, valamint havonta (a fogyasztási sor `Created` dátuma
|
||||
szerint), külön soron az „ismeretlen forrású" és a még készleten lévő rész.
|
||||
- [ ] **Leltári leírás mint fogyasztó** — a `StockTakingDbContext.CloseStockTaking` még nem hív a
|
||||
- [x] **Leltári leírás mint fogyasztó** — a `StockTakingDbContext.CloseStockTaking` még nem hív a
|
||||
`CostAttributionService`-be, ezért a leírt áru bent marad a rétegben, és a riport `Maradt` oszlopa
|
||||
készletnek mutatja. A `CostConsumerType.StockTakingItem` már létezik.
|
||||
*Kész (2026-08-14): a `CloseStockTaking` a készlet-frissítés mellett hívja az
|
||||
`AttributeStockTakingLossAsync`-et. Csak a **hiány** fogyaszt réteget — a leltári többlethez nem tartozik
|
||||
beszerzés, azt a modell nem tudja hova kötni. Menet közben javítva a riport `Maradt` oszlopa is: eddig az
|
||||
időszak eladásából számolt, ezért a korábbi hónapok fogyását és a leírást készletnek mutatta.*
|
||||
- [ ] **Előrendelés pontos forrása** — a `fbPreOrderItem` nem tárolja, melyik szállítólevél elégítette ki, ezért
|
||||
ott is FIFO dönt. Egy oszlop + egy értékadás a `PreOrderConversionService`-ben.
|
||||
- [ ] **Futásidejű ellenőrzés: devizás beszerzés önköltsége** — egy ÚJ, EUR-os beszállítótól származó
|
||||
|
||||
@@ -21,7 +21,11 @@ legrégebbitől. Ugyanabban a tranzakcióban fut, mint a készlet-levonás.
|
||||
nem mérendőnél **rekesz**. A sor a másik dimenziót is kitölti a tétel arányában.
|
||||
- Réteg híján a sor **ismeretlen forrású** (`ShippingItemId = null`), és naplóba is kerül. Nulla önköltséggel
|
||||
beolvasztani tilos — a riport külön során jelenik meg.
|
||||
- Idempotens: ha egy rendelés-tételhez már van fogyasztás, nem keletkezik újabb.
|
||||
- Idempotens: ha egy fogyasztóhoz már van fogyasztás-sor, nem keletkezik újabb.
|
||||
|
||||
Ugyanez a mechanizmus fut a **leltár-záráskor** (`CloseStockTaking`): a hiány FIFO szerint fogyasztja a rétegeket,
|
||||
`StockTakingItem` fogyasztó-fajtával. A leltári **többlet** nem fogyaszt — ahhoz nem tartozik beszerzés.
|
||||
A leírás így nem marad készletnek látszó mennyiség a `Maradt` oszlopban.
|
||||
|
||||
Tárolt adat egyedül az elfogyasztott mennyiség (`fbCostConsumption`). Minden pénzügyi érték lekérdezés, ezért
|
||||
egy később érkező helyesbített beszállítói vagy fuvarszámla **visszamenőleg átüt** a riporton.
|
||||
|
||||
@@ -73,7 +73,7 @@ OrderDto (order)
|
||||
|
||||
1. **Start Measuring** — `CustomOrderSignalREndpoint.StartMeasuring(orderId, userId)`
|
||||
- Sets `MeasurementOwnerId` via GenericAttribute
|
||||
- Order status set to Processing, Payment to Pending
|
||||
- Sends the "order started" customer notification — see [`NOTIFICATION/README.md`](NOTIFICATION/README.md)
|
||||
- Broadcasts `SendOrderChanged`
|
||||
2. **Weigh pallets** — `CustomOrderSignalREndpoint.AddOrUpdateMeasuredOrderItemPallet()`
|
||||
- Same weight formula as shipping
|
||||
@@ -100,16 +100,20 @@ valid = percentage < ProductDto.AverageWeightTreshold
|
||||
|
||||
`OrderDto.IsAllOrderItemAvgWeightValid` = all items pass this check.
|
||||
|
||||
### PendingMeasurementCheckoutFilter
|
||||
### Payment and the measurable item
|
||||
|
||||
ASP.NET Core action filter on the Checkout `ConfirmOrder` action.
|
||||
**An order containing a measurable item cannot be paid** — the final amount only exists after weighing. There is
|
||||
currently **no customer-facing checkout** in the system; when one is built (a Stripe integration is the plan), it
|
||||
may only accept payment for a measured, `Audited` order.
|
||||
|
||||
| Step | Action |
|
||||
|---|---|
|
||||
| Intercept | `ConfirmOrder` POST in CheckoutController |
|
||||
| Check | `OrderMeasurementService.IsPendingMeasurementAsync()` |
|
||||
| If pending | Set OrderStatus=Processing, PaymentStatus=Pending, redirect to PendingMeasurementWarning |
|
||||
| If not pending | Allow normal checkout flow |
|
||||
`PendingMeasurementCheckoutFilter` is what remains of that guard, and **today it guards nothing**:
|
||||
`OrderMeasurementService.IsPendingMeasurementAsync` reads an order-level `IsMeasurable` GenericAttribute that
|
||||
nobody writes any more — `OrderDto.IsMeasurable` is derived from the items (`OrderItemDtos.Any(oi => oi.IsMeasurable)`)
|
||||
and never stored — so the predicate is always `false`. The filter is bound to `ConfirmOrder`; were it to fire, it
|
||||
would redirect to `PendingMeasurementWarning`, and the `Processing` / `Pending` statuses it sets are not persisted.
|
||||
|
||||
Deliberately left unfixed while there is no customer checkout: the guard becomes part of that checkout when it is
|
||||
written, not of this filter.
|
||||
|
||||
## Workflow 3: StockTaking
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
# NOTIF — ISSUES
|
||||
|
||||
> ID format, Status vocabulary, type codes and archival are **not restated here** — see [`TOPIC_CODES.md`](../../.github/TOPIC_CODES.md) (→ framework registry).
|
||||
|
||||
Kontextus: [`README.md`](README.md).
|
||||
|
||||
---
|
||||
|
||||
## MGFBANKPLUG-NOTIF-I-Q3M8: A mérési értesítők kiváltása és idempotencia-jelölése
|
||||
|
||||
**Status:** Closed (2026-08-18) · **Priority:** P1 · **Type:** I · **2026-08-18**
|
||||
|
||||
Az [ADR 0003-H7C4](../adr/0003-H7C4-dto-event-bridge-wiring.md) horgony-bejegyzése. A rekord a DTO→fő-entitás
|
||||
esemény-híd bekötését rögzíti; ez a bejegyzés viszi az implementáció állapotát, a rekord *Follow-ups required*
|
||||
szakasza szerint.
|
||||
|
||||
**Mérés (2026-06-01 … 2026-08-19, a javítás előtt):** 3017 rendelés, 2916 lezárt, mind auditált raklappal —
|
||||
és mindössze 91 „értesítés megkísérelve" jelölés, 13 ténylegesen sorba tett levél.
|
||||
|
||||
### Lépések
|
||||
|
||||
- [x] **A híd bekötése** — `MgDtoDbTableBase` deklarálja a három `IConsumer<>` interfészt; `Mango.Nop.Data` →
|
||||
`Nop.Services` projektreferencia. Ezzel az `Audited` ág kiváltója helyreállt.
|
||||
- [x] **A `Started` ág triggere** — a mérés indítása (`FruitBankDbContext.StartMeasuringAsync`) csak
|
||||
`GenericAttribute`-ot ír, `OrderItemDtos`-ra pedig egyetlen írás sincs a pluginban, így a `Started`
|
||||
ablakban nem keletkezik `EntityUpdatedEvent<Order>`. A `CustomOrderSignalREndpoint.StartMeasuring` hívja
|
||||
közvetlenül, a mérésindítási tranzakció sikeres lezárulta után, a tranzakción kívül.
|
||||
- [x] **A jelölés akkor is ráíródott, ha a küldés nem sikerült** — a jelölés így azt jelentette, hogy
|
||||
„megpróbáltuk", nem azt, hogy „kiment", és mivel ott volt, a rendszer soha nem próbálta újra. Az
|
||||
idempotencia mostantól egyetlen helyen, a `FruitBankNotificationService.SendOnceAsync`-ben lakik, és
|
||||
mindkét hívó (a consumer `Audited` ága, valamint a `Started` triggert adó endpoint) azon át megy; a jelölés
|
||||
csak `queuedEmailId > 0` esetén íródik ki. Következmény: sikertelen küldés után a következő kiváltó
|
||||
újrapróbálkozik — tartósan hibás sablon- vagy fiók-konfigurációnál ez rendelés-frissítésenként egy újabb
|
||||
kísérletet jelent, amit a napló rendeléshez kötve jelez.
|
||||
- [x] **Visszamenőleg elégett jelölések — elfogadott jelenlegi állapot (2026-08-18)** — a sablonok
|
||||
2026-08-10-i helyreállítása előtt megkísérelt értesítések jelölése ott maradt a rendeléseken. **Nem
|
||||
takarítjuk:** a jelölés törlése azt eredményezné, hogy a következő Order-frissítésnél kimenne egy
|
||||
„elkészült, átvehető" levél hetekkel korábban átvett rendelésekre. A hiányzó levelek pótlása ezeknél
|
||||
nem cél.
|
||||
|
||||
### Resolution
|
||||
|
||||
Mind a négy lépés lezárva. A kiváltás két oldalról helyreállt — a keretbeli esemény-híd bekötésével
|
||||
([ADR 0003-H7C4](../adr/0003-H7C4-dto-event-bridge-wiring.md)) az `Audited`, a `StartMeasuring` endpoint
|
||||
közvetlen hívásával a `Started` ág —, az idempotencia pedig egyetlen helyre került, ahol a jelölés csak
|
||||
sikeres sorbaállítás után íródik ki. A visszamenőleg elégett jelöléseket szándékosan nem takarítjuk.
|
||||
|
||||
A mai állapot leírása: [`README.md`](README.md). **Éles ellenőrzés még hátravan:** a lezárás–jelölés–levél
|
||||
hármas újramérése néhány nap üzem után mondja meg, hogy a három szám együtt mozog-e.
|
||||
|
||||
### Érintett
|
||||
|
||||
- `Mango.Nop.Data/Repositories/MgDtoDbTableBase.cs`
|
||||
- `Services/EventConsumer.cs` — a `MeasuringStatus`-elágazás és a jelölés írása
|
||||
- `Domains/DataLayer/FruitBankDbContext.cs` — `StartMeasuringAsync`, `SetOrderStatusToCompleteAsync`
|
||||
@@ -0,0 +1,67 @@
|
||||
# ÉRTESÍTÉS — vevői e-mail értesítők
|
||||
|
||||
> Topic `NOTIF` (prefix `MGFBANKPLUG`) → entry ID-k `MGFBANKPLUG-NOTIF-<I|T|B>-<RAND>`. ID-formátum/Status-szótár → [`TOPIC_CODES.md`](../../.github/TOPIC_CODES.md) (→ keret-registry).
|
||||
> Companions: [`NOTIFICATION_ISSUES.md`](NOTIFICATION_ISSUES.md) — ismert defektek.
|
||||
|
||||
A plugin két saját vevői értesítőt küld a rendelés mérési állapotához kötve, a nop `MessageTemplate` /
|
||||
`QueuedEmail` gépezetén keresztül. A nop natív értesítői (OrderPlaced stb.) változatlanul működnek; ez a
|
||||
dokumentum azt írja le, amit a plugin tesz hozzá.
|
||||
|
||||
## A két sablon
|
||||
|
||||
| Sablon neve | Mikor | Törzs |
|
||||
|---|---|---|
|
||||
| `FruitBank.OrderStarted.CustomerNotification` | a rendelés mérése elindult | „elkezdtük feldolgozni" + mérendő tételnél árelőrejelzés |
|
||||
| `FruitBank.OrderAudited.CustomerNotification` | a rendelés lezárult (`OrderStatus.Complete`) | „elkészült, átvehető" + végösszeg |
|
||||
|
||||
A sablonok kanonikus alapértékét a `FruitBankPlugin.InstallAsync` írja be. **Már telepített pluginon ez nem fut
|
||||
le újra** — ezért éles adatbázisban a sablon kézzel is létrejöhet, eltérő tartalommal. A helyreállító runbook:
|
||||
[`../sql/fix_order_notification_templates.sql`](../sql/fix_order_notification_templates.sql).
|
||||
|
||||
## Kiváltás
|
||||
|
||||
A két értesítőt két különböző mechanizmus váltja ki.
|
||||
|
||||
A **`Started`** ág kiváltója nem esemény: a mérésindítás csak a `MeasurementOwnerId` GenericAttribute-ot írja,
|
||||
Order-sort nem, ezért a `CustomOrderSignalREndpoint.StartMeasuring` hívja közvetlenül, a mérésindítási
|
||||
tranzakció sikeres lezárulta után. A hívás a tranzakción kívül áll: visszagördülő mérésindításnál ne menjen ki
|
||||
levél.
|
||||
|
||||
Az **`Audited`** ág a `Services/EventConsumer`-en át jön, ami az `EntityUpdatedEvent<Order>`-re fut, és a
|
||||
rendelés **származtatott** `MeasuringStatus`-a alapján ágazik el. Order szinten ez az állapot az `OrderStatus`-ra
|
||||
kulcsol (`IsComplete` → `Audited`), nem a raklapok `RevisorId`-jára. A kiváltó maga a lezárás: a
|
||||
`SetOrderStatusToCompleteAsync` a DTO-táblán ír, ami a DTO→fő-entitás esemény-hídon át
|
||||
`EntityUpdatedEvent<Order>`-t szül (ADR 0003-H7C4).
|
||||
|
||||
Ismétlés ellen rendelésenként egy-egy `GenericAttribute` jelölés véd (`ORDER_STARTED_SENT_KEY`,
|
||||
`ORDER_AUDITED_SENT_KEY`). A jelölés kezelése egyetlen helyen, a `FruitBankNotificationService.SendOnceAsync`-ben
|
||||
van, és mindkét hívó azon át megy — a `Started` ágat ugyanis két kiváltó is elérheti.
|
||||
|
||||
A jelölés **csak sikeres sorbaállítás után** íródik ki. A küldés minden hibaágon `0`-t ad vissza (hiányzó vagy
|
||||
inaktív sablon, nincs használható e-mail fiók, nincs vevő); az ilyenkor is kiírt jelölés véglegesen elnémítaná a
|
||||
rendelést. Sikertelen küldés után a következő kiváltó újrapróbálkozik.
|
||||
|
||||
## Tokenek
|
||||
|
||||
A `FruitBankMessageTokenProvider` a nop `MessageTokenProvider`-ét váltja ki (`services.Replace`), és négy
|
||||
ponton tér el tőle:
|
||||
|
||||
- `Order.Product(s)` — saját HTML-tábla; mérendő tételnél ár helyett „kalkuláció alatt".
|
||||
- `Order.OrderTotal` — a nopnak **nincs** ilyen tokenje; a végösszeget csak a termék-tábla lábában jeleníti meg.
|
||||
A token értéke a `CustomPriceCalculationService` által az `Order.OrderTotal` oszlopba írt szám: bruttó,
|
||||
tételszinten már érvényesített kedvezménnyel, árfolyam-átszámítás nélkül.
|
||||
- `Order.MeasurableNote` — HTML-t hordozó token, amit a `FruitBankNotificationService` ad hozzá küldéskor.
|
||||
Az admin **teszt-küldésében** ezért üres marad.
|
||||
- `Customer.Company` — a `Company` valódi oszlop a `Customer` entitáson, a nop mégsem ad rá tokent; cégnevet
|
||||
csak a rendelés címeiből kínál (`Order.BillingCompany`). B2B-ben a vevő cége a rendeléstől független
|
||||
törzsadat. A token a vevő saját `Company` mezőjét adja, üresen hagyva, ha nincs kitöltve — a számlázási
|
||||
címre **nem** esik vissza.
|
||||
|
||||
A nop az admin „Engedélyezett tokenek" listáját a sablon nevéhez rendelt token-csoportokból állítja elő, és
|
||||
ismeretlen sablonnévre üres listát ad. A provider ezért felülírja a `GetTokenGroups`-ot a két FruitBank
|
||||
sablonnévre, és a `GetListOfAllowedTokensAsync`-ben fűzi a listához a két saját tokent.
|
||||
|
||||
## Naplózás
|
||||
|
||||
A küldés minden kimaradását a nop natív naplójába írjuk (Admin → Rendszer → Napló): hiányzó vagy inaktív
|
||||
sablon, hiányzó e-mail fiók, nem található vevő. Egy elmaradt vevői levél enélkül nyomtalan maradna.
|
||||
@@ -23,6 +23,7 @@ Topic documentation for the FruitBank-specific NopCommerce plugin (Layer 3 — c
|
||||
- [`EKAER/`](EKAER/README.md) — NAV EKÁER reporting, server side: obligation gate, `Shipping`/`Order` → `tradeCard` mapping, submission (+ `EKAER_ISSUES.md` / `EKAER_TODO.md`)
|
||||
- [`STOCK/`](STOCK/README.md) — stock movement: quantity + weight running balance, history and its consistency check (+ `STOCK_ISSUES.md`)
|
||||
- [`MARGIN/`](MARGIN/README.md) — purchase cost and margin: cost layers, FIFO attribution at order completion, freight/extra distribution, and the report (+ `MARGIN_TODO.md` / `MARGIN_ISSUES.md`, and `MARGIN_schema.sql`)
|
||||
- [`NOTIFICATION/`](NOTIFICATION/README.md) — customer e-mail notifications: the plugin's own message templates, the token-provider replacement, the `MeasuringStatus`-driven trigger and the repeat-guard flags (+ `NOTIFICATION_ISSUES.md`)
|
||||
- [`PMDATA/`](PMDATA/README.md) — product measurement/logistics master data (`IsMeasurable`, `Tare`, `AverageWeight`, `AverageWeightTreshold`, `CratesPerPallet`): storage, write paths, validation (+ `PMDATA_TODO.md`; the SQL runbook is the flat [`PRODUCT_WEIGHT_DATA_DB_VALIDATION.md`](PRODUCT_WEIGHT_DATA_DB_VALIDATION.md))
|
||||
|
||||
## Navigation
|
||||
|
||||
@@ -22,7 +22,8 @@ A mechanika (metódusok, hívási sorrend) a [`DATA_LAYER.md`](../DATA_LAYER.md)
|
||||
**A rendelés-lezárás készlet-mozgása költség-réteget is fogyaszt:** ugyanabban a tranzakcióban a
|
||||
`CostAttributionService` FIFO szerint hozzárendeli a kiadott árut ahhoz a beszerzéshez, amiből származik
|
||||
(`fbCostConsumption`) — ebből áll elő az árrés. Lásd [`MARGIN/README.md`](../MARGIN/README.md).
|
||||
A **leltár-zárás** ezt még nem teszi meg (`MGFBANKPLUG-MARGIN-T-K5V9`).
|
||||
A **leltár-zárás** ugyanígy fogyaszt: a `CloseStockTaking` a **hiányt** (negatív eltérést) rendeli a rétegekhez.
|
||||
A leltári **többlet** nem fogyaszt — ahhoz nem tartozik beszerzés, azt a modell nem tudja hova kötni.
|
||||
|
||||
## Nyitott munka
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -21,6 +21,7 @@ Az implementáció állapotát ADR-enként **pontosan egy** `_TODO` / `_ISSUES`
|
||||
|---|---|---|---|
|
||||
| [0001-V4T7](0001-V4T7-product-master-data-storage.md) | A termék mérési/logisztikai törzsadata elsőrendű, plugin-tulajdonú táblába kerül a `GenericAttribute`-ok helyett | Proposed (2026-08-11) | `MGFBANKPLUG-PMDATA-T-H3N8` ([PMDATA_TODO](../PMDATA/PMDATA_TODO.md)) — Open |
|
||||
| [0002-R7Q2](0002-R7Q2-document-cost-layers-and-margin.md) | A nyereség-kimutatás a szállítólevél szintjén számol, FIFO-val fogyasztott költség-rétegekből, és minden pénzügyi értéket lekérdezéssel állít elő | Proposed (2026-08-13) | `MGFBANKPLUG-MARGIN-T-K5V9` ([MARGIN_TODO](../MARGIN/MARGIN_TODO.md)) — InProgress |
|
||||
| [0003-H7C4](0003-H7C4-dto-event-bridge-wiring.md) | A DTO-repók esemény-hídja bekötésre kerül a keretben, a `Nop.Services` referencia árán | Accepted (2026-08-18) | `MGFBANKPLUG-NOTIF-I-Q3M8` ([NOTIFICATION_ISSUES](../NOTIFICATION/NOTIFICATION_ISSUES.md)) — Closed |
|
||||
|
||||
## Ezt a repót érintő döntések más rétegben
|
||||
|
||||
|
||||
Reference in New Issue
Block a user