Pricing module¶
The Pricing module (modules/pricing) is the quoting bounded context of the Cargonerds
solution. It turns a freight request (origin/destination, cargo, transport type) into one or more
priced offers by calling an external pricing engine, applying organisation-unit margins and
currency conversion, and persisting the resulting quotations. It is a self-contained ABP module
that the core application composes in through module dependencies,
and like the Hub module it follows ABP's layered architecture.
Pricing is a satellite of Hub
Pricing is not a standalone domain. Its aggregate roots (Quotation, Offer, ChargeLine,
Margin) live in Hub.Domain under the Hub.Entities.Pricing.* namespace and are persisted
by HubDbContext (connection string Hub). The Pricing.* projects are essentially an
application/UI/permissions layer on top of Hub: every Pricing layer depends on the matching Hub
layer (PricingDomainModule → HubDomainModule, PricingApplicationModule →
HubApplicationModule, …) and reuses Hub's repositories (IHubRepository<T, Guid>), base service
(HubEntityBaseService), DTOs (Hub.Dtos.Pricing.*) and Mapperly mappers. Read the
Hub module page first for the shared building blocks.
Projects and responsibilities¶
Each folder under modules/pricing/src is one layer of the module.
| Project | Purpose |
|---|---|
| Pricing.Domain.Shared | Shared constants and the PricingResource localization (Localization/Pricing). Hosts PricingDomainSharedModule. Referenced by all other Pricing projects. |
| Pricing.Domain | Module wiring and the small slice of pricing logic that is not an entity: PricingDomainModule (depends on HubDomainModule), the empty PricingSettings/PricingSettingDefinitionProvider scaffolds, PricingConsts, PricingDbProperties, and the CommentStatusChangedEventHandler. The Quotation/Offer/Margin aggregate roots themselves live in Hub.Domain. |
| Pricing.Application.Contracts | App-service interfaces (IQuotationAppService, IOfferAppService), the IPricingEngine Refit interface, the versioned legacy DTOs (Dtos/Legacy/*V1/*V4), CreateQuotationValidator, and permission definitions (PricingPermissions, PricingPermissionDefinitionProvider). |
| Pricing.Application | App-service implementations (QuotationAppService, OfferAppService, QuotationLegacyAppService), the QuoteGenerationJob background job, supporting services (CalculationService, CurrencyService, MarginCalculationService), the QuotationSearchProvider, the QuoteNumberGenerator helper, and the Mapperly QuoteMapper. |
| Pricing.EntityFrameworkCore | PricingDbContext / IPricingDbContext and PricingEntityFrameworkCoreModule. Depends on HubEntityFrameworkCoreModule; has no DbSets — see the note below. |
| Pricing.HttpApi | The PricingController base, the IPricingEngine Refit registration (PricingHttpApiModule) and the PricingEngineAuthHandler. |
| Pricing.HttpApi.Client | Strongly typed C# client proxies for the Pricing APIs (PricingHttpApiClientModule). |
| Pricing.Blazor | A Razor/Blazor page library: PricingBlazorModule, PricingMenuContributor, and the quotation pages — which are now redirect stubs (see User interface). |
| Pricing.Blazor.WebAssembly + Pricing.Blazor.WebAssembly.Bundling | WebAssembly host module and the (currently empty) bundle script/style contributors. |
| Pricing.Installer | Helper module used when adding Pricing to a host; only embeds its own files in the virtual file system. |
Domain overview¶
The Pricing domain revolves around aggregate roots that are all defined in Hub.Domain
(modules/hub/src/Hub.Domain/Entities/Pricing) and derive from Hub's CustomerOwnedGuidEntity, so
they participate in Hub's per-customer query filtering. See
Entities & aggregate roots
for the ABP base classes.
Hub.Entities.Pricing.Quotation— the quote request and its result. It implementsIUserOwned,ISoftDeleteandISearchable<Quotation>. It carries the request side (transport type, requested ports/addresses,PackLineRequest/ContainerRequestline items, commodity, incoterm, currency, calculation flags and quoting-source selection) and the response side (MovementType, theList<Offer> Offers, validity timeframe and aQuoteStatus Status). It is searchable byQuoteNumberandLegacyQuoteNumber.Hub.Entities.Pricing.Response.Offer— a single priced result for a quotation. It links back to itsQuotation, names the carrier/SCAC, origin/destinationPorts, aList<ChargeLine> Charges,decimal TotalCosts,Currency, transit time and validity window.Hub.Entities.Pricing.Response.ChargeLine— one charge on an offer:ChargeBase/ChargeNet/ChargeGross,Currency/CurrencyBase,bool IsMarginOnly, and a many-to-many back-referenceICollection<Margin>? AppliedMargins.Hub.Entities.Pricing.Margin— a markup rule:TransportType,MarginApplication Application,ChargeCode,Rate, aMarginRateCalculator RateCalculator,MinimumCharge, optionalPlace? Origin/Destination(a TPHPlacehierarchy:PortPlace/CountryPlace/AddressPlace), andbool IsDefault.
namespace Hub.Entities.Pricing;
public class Quotation : CustomerOwnedGuidEntity, IUserOwned, ISoftDelete, ISearchable<Quotation>
{
public string QuoteNumber { get; set; } = string.Empty;
public Guid OrganizationUnitId { get; set; }
public QuoteStatus Status { get; set; } = QuoteStatus.Undefined;
public TransportType TransportType { get; set; }
public virtual List<Offer> Offers { get; set; } = [];
// ... request, calculation and response properties
}
The pricing entities are mapped via IEntityTypeConfiguration<> classes under
modules/hub/src/Hub.EntityFrameworkCore/EntityFrameworkCore/TypeConfigurations/Pricing/
(QuoteConfiguration, OfferConfiguration, ChargeLineConfiguration, MarginConfiguration,
MarginRateCalculatorConfiguration). Margin↔ChargeLine is a many-to-many
(HasMany(s => s.AppliedOn).WithMany(s => s.AppliedMargins)), and all FK relationships use
OnDelete(DeleteBehavior.Restrict).
Quote lifecycle¶
QuoteStatus (modules/hub/src/Hub.Domain.Shared/Enums/Pricing/QuoteStatus.cs) drives the
workflow. ABP's auto-API serialises enums as integers by default, so these are the values clients
see on the wire:
| Value | Name | Meaning |
|---|---|---|
| 0 | Undefined |
Default / unset. |
| 1 | Draft |
Created but not yet calculated. The only state from which CreateQuoteAsync will start generation. |
| 2 | Open |
Calculation finished (offers priced, or a manual quote was set). |
| 3 | Request |
A quote-request comment is open (set by CommentStatusChangedEventHandler). |
| 4 | Booked |
Quote has been booked. |
| 5 | Calculating |
Generation in progress; the background job is running. |
stateDiagram-v2
[*] --> Draft: CreateAsync
Draft --> Calculating: CreateQuoteAsync (enqueues job)
Calculating --> Open: offers priced or manual quote
Open --> Request: open quote-request comment
Request --> Open: all comments resolved
Open --> Booked: booking
Application services¶
The application layer
is the entry point for clients. All three services live in modules/pricing/src/Pricing.Application/Quotation
and use primary-constructor dependency injection.
QuotationAppService(IQuotationAppService) extends Hub'sHubEntityBaseService<Quotation, QuotationDto, QuotationAppService>(Hub's wrapper over ABP's CRUD app service). The interface is anICrudAppService<QuotationDto, Guid, PagedAndSortedResultRequestWithFacetOptionsDto, QuotationCreateDraftDto, QuotationDraftDto>plusCreateQuoteAsync,GetStatusAsyncandInvalidateQuoteCacheAsync. It operates onIHubRepository<Hub.Entities.Pricing.Quotation, Guid>and is guarded byPricingPermissions.OfferAppService(IOfferAppService) is theHubEntityBaseService<Offer, OfferDto, OfferAppService>for manual offer CRUD; it recomputesTotalCostsviaCalculationService.QuotationLegacyAppServiceis the engine of the module — it calls the external pricing API, maps the response, applies margins and sets totals/status. It is registered with[ExposeServices(typeof(QuotationLegacyAppService))]+[Dependency(ServiceLifetime.Transient)], and itsCreateQuoteAsyncis[AllowAnonymous]and[DisableValidation](it is only invoked from the background job, not exposed as a normal endpoint).
These build on the supporting services in Pricing.Application/Services and …/Margins:
| Service | Responsibility |
|---|---|
CalculationService |
GetTotalCosts(charges, outputCurrency) sums each charge's ChargeGross ?? ChargeNet, converting via CurrencyService. |
CurrencyService |
Loads the latest EUR-referenced exchange rates from HubDbContext and converts amounts. Internal — [ExposeServices(typeof(CurrencyService))], not exposed to Swagger. |
MarginCalculationService |
Applies organisation-unit margins to the raw pricing-engine offers (see Margin engine). |
QuotationSearchProvider |
A SearchProviderBase<Quotation, QuotationSearchResultItemDto> registered as an IGlobalSearchProvider (Order = 4, requires PricingPermissions.Quotation.View); contributes quotations to Hub's global search. |
QuoteNumberGenerator |
Generates human-readable quote/offer numbers (see the gotcha). |
Quote generation flow¶
This is the important path. A draft quotation is created synchronously; pricing happens asynchronously in a background job so the HTTP request returns immediately.
sequenceDiagram
participant C as Client
participant Q as QuotationAppService
participant J as QuoteGenerationJob (Hangfire)
participant L as QuotationLegacyAppService
participant E as External pricing engine
participant M as MarginCalculationService
C->>Q: CreateAsync(draft)
Q-->>C: Quotation (Status=Draft)
C->>Q: CreateQuoteAsync(id)
Q->>Q: Status = Calculating
Q->>J: EnqueueAsync(args)
Q-->>C: Quotation (Status=Calculating)
J->>L: CreateQuoteAsync(id) [new UoW]
L->>E: POST /api/public/pricing/v4
E-->>L: PublicApiQuoteResponseV4
L->>M: CalculateMargins(per offer)
L->>L: Status = Open, save
J->>J: invalidate caches, notify user
QuotationAppService.CreateAsync(QuotationCreateDraftDto)validates org-unit access (ValidateUserAccessToOrgUnitAsync), strips non-existent ports, maps the DTO with Mapperly (input.ToNet(new SuperTypeResolutionWithDbLoadingReferenceHandler(dbContext))), assigns aQuoteNumber,UserId,Status = Draft, and saves.-
QuotationAppService.CreateQuoteAsync(quotationId)runs only ifStatus == Draft. It flips the status toCalculating, then enqueues the job. Note the notification flag is derived from a permission check:entity.Status = QuoteStatus.Calculating; await dbContext.SaveChangesAsync(ct); bool sendNotification = await permissionChecker.IsGrantedAsync(PricingPermissions.Quotation.Admin); await backgroundJobManager.EnqueueAsync( new QuoteGenerationJobArgs { QuotationId = quotationId, SendNotification = sendNotification, TriggeredByUserId = CurrentUser.Id, } ); -
QuoteGenerationJob([Queue(HangfireQueues.QuoteGenerationQueue)], anAsyncBackgroundJob<QuoteGenerationJobArgs>) impersonates the triggering user viaICurrentPrincipalAccessor.Change(...), runs the legacy service inside a new unit of work (unitOfWorkManager.Begin(requiresNew: true)), invalidates the offer + quote caches in afinallyblock, then fires a fire-and-forgetNotificationSenderServicejob: -
QuotationLegacyAppService.CreateQuoteAsyncloads the quotation with deep includes (IncludeDefaultDetails()+ offers/charges/charge-code/currency). It short-circuits to a manual quote (QuoteMapper.SetManualQuote) when origin/destination is missing, or when an FCL quote uses a container type outside the supported list. Otherwise it buildsquotation.ToLegacy(), sets the output currency, POSTs to the engine, maps the response, runs margins + totals per offer, and setsStatus = Open:PublicApiQuoteRequestV4 legacyRequest = quotation.ToLegacy(); legacyRequest.OutputCurrency = quotation.Currency.Code; var client = new HttpClient(); client.Timeout = TimeSpan.FromMinutes(2); client.BaseAddress = new Uri(whiteLabelingOptions.ExternalApis?.PricingManager?.Url ?? string.Empty); client.DefaultRequestHeaders.Add("ApiKey", whiteLabelingOptions.ExternalApis?.PricingManager?.ApiKey); HttpResponseMessage response = await client.PostAsync( "/api/public/pricing/v4", new StringContent(JsonSerializer.Serialize(legacyRequest), Encoding.UTF8, "application/json"), ct);If the engine returns no offers, it falls back to
SetManualQuote. On success it callslegacyResponse.ToNewQuoteResponse(...), then for each offer runsmarginCalculationService.CalculateMargins(...)followed bycalculationService.GetTotalCosts(...), setsOfferCount,Status = Open, adds the new charge lines todbContext.ChargeLinesand saves.
The external pricing engine¶
The engine ("PricingManager") is configured under
WhiteLabelingOptions.ExternalApis.PricingManager (Url + ApiKey), defined in
modules/hub/src/Hub.Domain.Shared/WhiteLabeling/WhiteLabelingOptions.classes.cs:
There are two code paths to this engine, and only one of them actually runs:
- A Refit interface
IPricingEngine([Post("/api/public/pricing/v4")]) is registered inPricingHttpApiModule— only ifPricingManager.Urlis set — withPricingEngineAuthHandler(adds theApikeyheader) and a 15-minute timeout. - The live path in
QuotationLegacyAppServiceuses a hand-rollednew HttpClient()with a 2-minute timeout and theApiKeyheader set inline.
The Refit client is configured but never consumed
IPricingEngine is registered in PricingHttpApiModule but is not injected anywhere — the
actual engine call is the raw HttpClient in QuotationLegacyAppService. The two paths also use
different timeouts (15 min vs 2 min), and the Refit registration additionally rewrites the global
HttpStandardResilienceOptions keyed "standard" (4-min attempt / 14-min total / 2 retries),
which — as the in-code comment notes — affects all HTTP clients in the host, not just pricing.
Margin engine¶
MarginCalculationService.CalculateMargins (modules/pricing/src/Pricing.Application/Margins/MarginCalculationService.cs)
marks up an offer's charges. It is pure-ish math over data loaded from Hub:
- Load margins via
IMarginRepository.GetMarginsAsync(orgUnitId). This returns the org unit's margins, falling back toIsDefaultmargins only when the org unit has none (a partial set suppresses defaults entirely). DetermineApplicableMarginsfilters the margins by, in order:- Transport type (
margin.TransportType == quotationData.TransportType); - FCL container type —
Per20/40/40HcFeetContainercalculators only apply when the requested container code matches22G1/42G1/45G1respectively; - Charge code — a margin applies if it
CreateNewCharges, or the offer already has a charge with the margin's charge code; - Best route match — margins are grouped by
(TransportType, ChargeCode)and the highest scoring one per group wins, usingGetMatchScore: an exactPortPlacematch scores 2, aCountryPlacematch 1, anull"any" place 0, and a mismatch -1 (which disqualifies the margin).
- Transport type (
CalculateMarginChargescomputes a base charge perCalculatorMargin(per HBL, per container, per TEU, per CBM, per kg, per chargeable kg, per 100 lbs, per volume-weight, percent, …), converts it to the output currency viaCurrencyService, and applies theMinimumCharge.- Apply the result either as
IncreaseExistingCharge(raises the matching charge'sChargeGross) orCreateNewCharge(adds anIsMarginOnlyChargeLine). Applied margins are recorded onChargeLine.AppliedMargins.
Container codes are hardcoded
Only three FCL container codes are supported — 22G1 (20'), 42G1 (40'), 45G1 (40'HC) — and
they appear as constants in both MarginCalculationService.CONTAINER_LIST and the legacy
mapper. An FCL quote with any other container type is forced to a manual quote.
Currency conversion¶
CurrencyService reads HubDbContext.ExchangeRates, taking the latest TargetDate per currency
where ReferenceCurrency == "EUR" into a Dictionary<string, decimal>. GetConversionCostsWithLookup
pivots every conversion through EUR (from → EUR → to):
private static string CONVERSION_REFERENCE_CURRENCY = "EUR";
// ...
var costsEur = fromCode.Equals(CONVERSION_REFERENCE_CURRENCY) ? costs : costs / fromRate;
return toCode.Equals(CONVERSION_REFERENCE_CURRENCY) ? costsEur : costsEur * toRate;
A missing non-EUR rate throws InvalidOperationException. Percent margins are computed in the
charge's currency, then the minimum is converted into the margin's currency — see the gotcha
below.
Permissions¶
Permissions are declared in Pricing.Application.Contracts and registered by
PricingPermissionDefinitionProvider under the Pricing group. They use Hub's [GrantInRoles]
attribute to seed default role grants. See ABP
authorization.
namespace Pricing.Permissions;
public class PricingPermissions
{
public const string GroupName = "Pricing";
public static class Quotation
{
[GrantInRoles(RoleConsts.DefaultCustomer)]
public const string View = QuotationGroup + ".View";
[GrantInRoles(RoleConsts.DefaultCustomer)]
public const string Add = QuotationGroup + ".Add";
[GrantInRoles(RoleConsts.AccountOwner)]
public const string Edit = QuotationGroup + ".Edit";
[GrantInRoles(RoleConsts.AccountOwner)]
public const string Delete = QuotationGroup + ".Delete";
[GrantInRoles(RoleConsts.Operator)]
public const string Admin = QuotationGroup + ".Admin";
[GrantInRoles(RoleConsts.DefaultCustomer, Inherit = false)]
public const string QuoteRequest = QuotationGroup + ".QuoteRequest";
}
public static class Offer { /* OfferGroup, Add, Edit, Delete — Operator */ }
}
The Offer permissions are registered as children of the Quotation permission, and the provider
localises their display names through PricingResource. Note Quotation.Admin doubles as the
"send-notification on quote completion" flag (see step 2 above).
EF Core and where the data lives¶
Pricing.EntityFrameworkCore defines PricingDbContext (and IPricingDbContext), both annotated
[ConnectionStringName(PricingDbProperties.ConnectionStringName)] ("Pricing"), and
PricingEntityFrameworkCoreModule registers it with
options.AddDefaultRepositories<IPricingDbContext>(includeAllEntities: true).
Quotation/Offer data is stored via the Hub context — PricingDbContext is empty
PricingDbContext declares no DbSets, and its OnModelCreating calls
builder.ConfigurePricing() which is a no-op placeholder (a commented-out template body). The
Quotation, Offer, ChargeLine, Margin and MarginRateCalculator entities are DbSets on
HubDbContext and their schema is created by Hub's
ApplyMultiTypeConfigurationsFromContextAssembly(...). That is why
PricingEntityFrameworkCoreModule depends on HubEntityFrameworkCoreModule and the app services
inject IHubRepository<Hub.Entities.Pricing.Quotation, Guid> rather than a Pricing repository.
The Pricing-prefixed tables are part of the Hub migration set, not a separate "Pricing"
database. See Entity Framework Core and
Migrations.
User interface¶
The Pricing UI is the Pricing.Blazor Razor/Blazor page library. PricingBlazorModule registers
the menu contributor, an AutoMapper profile and AbpRouterOptions.AdditionalAssemblies for routing,
and Pricing.Blazor.WebAssembly is the WebAssembly host module
(LeptonX theme).
Quotation UI has moved to Hub.UI
The quotation list and quotation screens are now served from Hub.UI at /hub/quotations,
/hub/quotation/{id} and /hub/quotation/create. PricingMenuContributor.ConfigureMainMenuAsync
adds no menu items, and the Pricing.Blazor quotation pages (Quotations, QuotationCreate,
QuotationDetail, QuotationCompare) are redirect stubs that forward to the Hub.UI
equivalents. The WASM bundle script/style contributors are empty, and Index.razor /
ExampleController are leftover ABP template samples. New quotation UI work should go into
Hub.UI. See the Blazor UI page.
How the Pricing module is integrated¶
Pricing is composed into the core via ABP module dependencies — but, unusually, the host does not
[DependsOn] the Pricing application/EF modules directly. Instead it is pulled in through the
client/WASM/domain dependency chains plus conventional-controller registration:
| Integration point | File | What it does |
|---|---|---|
CargonerdsDomainModule [DependsOn(PricingDomainModule)] |
src/Cargonerds.Domain/CargonerdsDomainModule.cs |
Brings in the Pricing domain; also registers the CmsKit comment entity type new CommentEntityTypeDefinition(PricingConsts.QuotationRequest). |
| Conventional controllers | src/Cargonerds.HttpApi.Host/CargonerdsHttpApiHostModule.cs |
options.ConventionalControllers.Create(typeof(PricingApplicationModule).Assembly) exposes QuotationAppService/OfferAppService as auto-API REST controllers. |
CargonerdsHttpApiClientModule [DependsOn(PricingHttpApiClientModule)] |
src/Cargonerds.HttpApi.Client/… |
Wires the typed client proxies. |
CargonerdsBlazorClientModule [DependsOn(PricingBlazorWebAssemblyModule)] |
src/Cargonerds.Blazor.Client/… |
Wires the Blazor WASM UI. |
CargonerdsDbContext builder.ConfigurePricing() |
src/Cargonerds.EntityFrameworkCore/EntityFrameworkCore/CargonerdsDbContext.cs (line 106) |
Resolves to the no-op placeholder (via using Pricing.EntityFrameworkCore;) — it does nothing for pricing entities; the real mapping is on HubDbContext. |
graph TD
PDS[PricingDomainSharedModule] --> PD[PricingDomainModule]
HD[HubDomainModule] --> PD
PD --> PAC[PricingApplicationContractsModule]
HAC[HubApplicationContractsModule] --> PAC
PAC --> PA[PricingApplicationModule]
HA[HubApplicationModule] --> PA
PD --> PEF[PricingEntityFrameworkCoreModule]
HEF[HubEntityFrameworkCoreModule] --> PEF
PAC --> PB[PricingBlazorModule]
Because the Pricing aggregate roots are Hub entities, their tables are created in the same database
when Cargonerds.DbMigrator runs (through HubDbContext). For running and deploying the system, see
the deployment guide; for the shared layering conventions see the
DDD overview, aggregate roots and the
layered architecture pages.
Quote-status side channel via comments¶
A QuotationRequest is registered as a CmsKit comment entity type (the key is
PricingConsts.QuotationRequest). CommentStatusChangedEventHandler
(modules/pricing/src/Pricing.Domain/EventHandler/CommentStatusChangedEventHandler.cs) is an
IDistributedEventHandler<CommentExtraPropertiesUpdatedEto> — see ABP's
distributed event bus.
When a comment on a quotation-request changes status it recomputes the quotation status: Request if
any comment is still Open, otherwise Open.
Mapping¶
The entity↔DTO mapping is done with Mapperly, not AutoMapper. QuoteMapper
(modules/pricing/src/Pricing.Application/Extensions/QuoteMapper.cs) is a
[Mapper(UseReferenceHandling = true)] [UseStaticMapper(typeof(DtoMapper))] static partial class
providing ToNet/ToDto for Quotation/Offer/ChargeLine, plus the hand-written
SetQuoteResponse/SetManualQuote/ToNewQuoteResponse translations from the legacy V4 response.
A custom SuperTypeResolutionWithDbLoadingReferenceHandler resolves/attaches existing entities by
id/code during mapping. An ABP AbpAutoMapperOptions profile is also configured but is secondary. See
ABP object-to-object mapping & Mapperly
and the sibling DTOs page.
Gotchas¶
Hardcoded production API key in config
WhiteLabelingOptions:ExternalApis:PricingManager:ApiKey is committed as a literal value (with a
prod URL) in src/Cargonerds.HttpApi.Host/appsettings.spark.prod.json,
appsettings.spark.prod-read-only.json and appsettings.spark.test.json. This secret should be
rotated and moved out of source control.
- The domain lives in Hub, not
Pricing.Domain.Pricing.Domainhas no entities;PricingDbContext(connectionPricing) has noDbSets;ConfigurePricing()is a no-op. Pricing migrations/tables belong to the Hub/Cargonerds migration set. builder.ConfigurePricing()inCargonerdsDbContextdoes nothing — it resolves to the placeholder extension. Real mapping is onHubDbContext.- The Refit
IPricingEngineclient is configured but never injected — the live call uses a rawHttpClient. The two paths have divergent timeouts (15 min vs 2 min), and the Refit registration rewrites the global"standard"resilience options for all clients. QuoteNumberGeneratorre-seeds the DB sequence to a random 0–200 on every call ("Legacy: We start at a random value to simulate more use"). Quote/offer numbers (Q/O+yyMMdd+ 5 digits) are therefore not monotonic and uniqueness is not guaranteed by this generator.- Margin currency rules are subtle. Non-
Percentcalculators requiremargin.Currency;Percentonly requires it whenMinimumCharge > 0. Percent margins compute in the charge's currency, then the minimum is converted into the margin currency — easy to break when editing. GetMarginsAsyncfalls back to default margins only when the org unit has zero margins. A partial set for the org unit suppresses defaults entirely.- Many
Quotationrequest flags are gated off.CreateQuotationValidatorenforces "unsupported" features (DangerousGoods, Insurance, etc.) to befalse, requiresTransportType != Undefined, and limits FCL to a single distinct container type. - Test projects are still ABP template scaffolds (
Samples/SampleAppService_Tests, …) undermodules/pricing/test/*— there is no real test coverage of the quote/margin logic. See Testing.
Localization and translations¶
Like the core and Hub, the Pricing module ships a Localization/Pricing folder under
Pricing.Domain.Shared exposing PricingResource. Permission and UI strings resolve through this
resource (LocalizableString.Create<PricingResource>), and PricingHttpApiModule adds
AbpUiResource as a base type. Add new keys to en.json and translate them with
Resourcetranslator.Cli:
resourcetranslator translate \
--options modules/pricing/src/Pricing.Domain.Shared/translation.options.json \
--input modules/pricing/src/Pricing.Domain.Shared/Localization/Pricing/en.json \
--output modules/pricing/src/Pricing.Domain.Shared/Localization/Pricing
ABP loads the resulting culture files automatically at runtime — see Localization and the sibling Localization UI page.