Skip to content

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 (PricingDomainModuleHubDomainModule, PricingApplicationModuleHubApplicationModule, …) 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 implements IUserOwned, ISoftDelete and ISearchable<Quotation>. It carries the request side (transport type, requested ports/addresses, PackLineRequest/ContainerRequest line items, commodity, incoterm, currency, calculation flags and quoting-source selection) and the response side (MovementType, the List<Offer> Offers, validity timeframe and a QuoteStatus Status). It is searchable by QuoteNumber and LegacyQuoteNumber.
  • Hub.Entities.Pricing.Response.Offer — a single priced result for a quotation. It links back to its Quotation, names the carrier/SCAC, origin/destination Ports, a List<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-reference ICollection<Margin>? AppliedMargins.
  • Hub.Entities.Pricing.Margin — a markup rule: TransportType, MarginApplication Application, ChargeCode, Rate, a MarginRateCalculator RateCalculator, MinimumCharge, optional Place? Origin/Destination (a TPH Place hierarchy: PortPlace/CountryPlace/AddressPlace), and bool 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). MarginChargeLine 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's HubEntityBaseService<Quotation, QuotationDto, QuotationAppService> (Hub's wrapper over ABP's CRUD app service). The interface is an ICrudAppService<QuotationDto, Guid, PagedAndSortedResultRequestWithFacetOptionsDto, QuotationCreateDraftDto, QuotationDraftDto> plus CreateQuoteAsync, GetStatusAsync and InvalidateQuoteCacheAsync. It operates on IHubRepository<Hub.Entities.Pricing.Quotation, Guid> and is guarded by PricingPermissions.
  • OfferAppService (IOfferAppService) is the HubEntityBaseService<Offer, OfferDto, OfferAppService> for manual offer CRUD; it recomputes TotalCosts via CalculationService.
  • QuotationLegacyAppService is 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 its CreateQuoteAsync is [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
  1. QuotationAppService.CreateAsync(QuotationCreateDraftDto) validates org-unit access (ValidateUserAccessToOrgUnitAsync), strips non-existent ports, maps the DTO with Mapperly (input.ToNet(new SuperTypeResolutionWithDbLoadingReferenceHandler(dbContext))), assigns a QuoteNumber, UserId, Status = Draft, and saves.
  2. QuotationAppService.CreateQuoteAsync(quotationId) runs only if Status == Draft. It flips the status to Calculating, 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,
        }
    );
    
  3. QuoteGenerationJob ([Queue(HangfireQueues.QuoteGenerationQueue)], an AsyncBackgroundJob<QuoteGenerationJobArgs>) impersonates the triggering user via ICurrentPrincipalAccessor.Change(...), runs the legacy service inside a new unit of work (unitOfWorkManager.Begin(requiresNew: true)), invalidates the offer + quote caches in a finally block, then fires a fire-and-forget NotificationSenderService job:

    using (var uow = unitOfWorkManager.Begin(requiresNew: true))
    {
        quotation = await legacyService.CreateQuoteAsync(args.QuotationId);
        await uow.CompleteAsync();
    }
    
  4. QuotationLegacyAppService.CreateQuoteAsync loads 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 builds quotation.ToLegacy(), sets the output currency, POSTs to the engine, maps the response, runs margins + totals per offer, and sets Status = 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 calls legacyResponse.ToNewQuoteResponse(...), then for each offer runs marginCalculationService.CalculateMargins(...) followed by calculationService.GetTotalCosts(...), sets OfferCount, Status = Open, adds the new charge lines to dbContext.ChargeLines and 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:

public class Pricingmanager
{
    public string Url { get; set; }
    public string ApiKey { get; set; }
}

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 in PricingHttpApiModuleonly if PricingManager.Url is set — with PricingEngineAuthHandler (adds the Apikey header) and a 15-minute timeout.
  • The live path in QuotationLegacyAppService uses a hand-rolled new HttpClient() with a 2-minute timeout and the ApiKey header 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:

  1. Load margins via IMarginRepository.GetMarginsAsync(orgUnitId). This returns the org unit's margins, falling back to IsDefault margins only when the org unit has none (a partial set suppresses defaults entirely).
  2. DetermineApplicableMargins filters the margins by, in order:
    • Transport type (margin.TransportType == quotationData.TransportType);
    • FCL container typePer20/40/40HcFeetContainer calculators only apply when the requested container code matches 22G1 / 42G1 / 45G1 respectively;
    • 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, using GetMatchScore: an exact PortPlace match scores 2, a CountryPlace match 1, a null "any" place 0, and a mismatch -1 (which disqualifies the margin).
  3. CalculateMarginCharges computes a base charge per CalculatorMargin (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 via CurrencyService, and applies the MinimumCharge.
  4. Apply the result either as IncreaseExistingCharge (raises the matching charge's ChargeGross) or CreateNewCharge (adds an IsMarginOnly ChargeLine). Applied margins are recorded on ChargeLine.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.Domain has no entities; PricingDbContext (connection Pricing) has no DbSets; ConfigurePricing() is a no-op. Pricing migrations/tables belong to the Hub/Cargonerds migration set.
  • builder.ConfigurePricing() in CargonerdsDbContext does nothing — it resolves to the placeholder extension. Real mapping is on HubDbContext.
  • The Refit IPricingEngine client is configured but never injected — the live call uses a raw HttpClient. The two paths have divergent timeouts (15 min vs 2 min), and the Refit registration rewrites the global "standard" resilience options for all clients.
  • QuoteNumberGenerator re-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-Percent calculators require margin.Currency; Percent only requires it when MinimumCharge > 0. Percent margins compute in the charge's currency, then the minimum is converted into the margin currency — easy to break when editing.
  • GetMarginsAsync falls back to default margins only when the org unit has zero margins. A partial set for the org unit suppresses defaults entirely.
  • Many Quotation request flags are gated off. CreateQuotationValidator enforces "unsupported" features (DangerousGoods, Insurance, etc.) to be false, requires TransportType != Undefined, and limits FCL to a single distinct container type.
  • Test projects are still ABP template scaffolds (Samples/SampleAppService_Tests, …) under modules/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.