PunchoutExtension reference
Sana.Extensions.Punchout.PunchoutExtension
is an abstract class that you should implement
when you develop a new punchout extension for Sana Commerce.
Properties
PunchoutId
Gets the identifier of punchout extension. The identifier should be unique among all other punchout extensions.
Example:
public override string PunchoutId => "CustomPunchout";
Methods
GetSettings
Gets the punchout settings. The method returns an instance of PunchoutSettings. Implement this method and pass values from extension configuration if need to change the default values described here.
Example:
public override PunchoutSettings GetSettings() => new()
{
LoginErrorPageLink = Configuration.LoginErrorPageLink,
ShippingMethodId = Configuration.ShippingMethodId,
IsOrderConfirmationEmailEnabled = Configuration.IsOrderConfirmationEmailEnabled,
IsLogoutAllowed = Configuration.IsLogoutAllowed,
SaveNewShippingAddressesToCustomerAddresses = Configuration.SaveNewShippingAddressesToCustomerAddresses
};
OnLoginRequestReceivedAsync
Handles login request and identifies information required to login shop account.
Sana calls this method and passes the instance of PunchoutLoginRequestContext with HTTP request information and the method returns an instance of PunchoutLoginInfo with information to login shop account.
Example:
public override async Task<PunchoutLoginInfo> OnLoginRequestReceivedAsync(PunchoutLoginRequestContext context, CancellationToken cancellationToken)
{
var punchoutSessionId = context.Request.Query["sessionId"];
var requestUri = "https://your-punchout-service-provider/login";
var requestContent = JsonSerializer.Serialize(new LoginRequest(punchoutSessionId), jsonSerializerOptions);
using var httpClient = new HttpClient();
var response = await httpClient.PostAsync(
requestUri,
new StringContent(requestContent, Encoding.UTF8, "application/json"),
cancellationToken);
await ValidateHttpResponseAsync(response, requestUri, requestContent, cancellationToken);
var responseContent = await response.Content.ReadAsStringAsync(cancellationToken);
var loginResponse = JsonSerializer.Deserialize<LoginResponse>(responseContent, jsonSerializerOptions)!;
var basketRestoreInfo = loginResponse?.Basket is null
? null
: BasketRestoreInfoConverter.Convert(loginResponse.Basket);
return new(punchoutSessionId, loginResponse.ShopAccountEmail, Configuration.StartPageLink, basketRestoreInfo);
}
static async Task ValidateHttpResponseAsync(HttpResponseMessage response, string requestUri, string requestContent, CancellationToken cancellationToken)
{
if (!response.IsSuccessStatusCode)
{
var errorMessage = $"Error occurred while communicating with {requestUri}. Status code: {response.StatusCode}. Request content: '{requestContent}'.";
var responseContent = await response.Content.ReadAsStringAsync(cancellationToken);
if (!string.IsNullOrEmpty(responseContent))
errorMessage += $" Response content: '{responseContent}'.";
throw new InvalidOperationException(errorMessage);
}
}
record LoginRequest([property: JsonPropertyName("sessionId")] string SessionId);
record LoginResponse(
[property: JsonPropertyName("email")] string ShopAccountEmail,
[property: JsonPropertyName("basket")] BasketInput? Basket);
record BasketInput(
[property: JsonPropertyName("salesAgreementId")] string? SalesAgreementId,
[property: JsonPropertyName("lines")] IReadOnlyCollection<BasketLineInput>? Lines);
record BasketLineInput(
[property: JsonPropertyName("productId")] string? ProductId,
[property: JsonPropertyName("variantId")] string? VariantId,
[property: JsonPropertyName("unitOfMeasureId")] string? UnitOfMeasureId,
[property: JsonPropertyName("quantity")] decimal Quantity,
[property: JsonPropertyName("salesAgreementLineId")] string? SalesAgreementLineId,
[property: JsonPropertyName("configurationId")] long? ConfigurationId);
TransferBasketAsync
Transfers contents of the basket to the procurement system.
Sana calls this method and passes the instance of PunchoutBasketTransferContext with information needed to transfer basket to the procurement system and the method returns an instance of PunchoutNextAction with an action identifying what should be done next.
Example:
public override async Task<PunchoutNextAction> TransferBasketAsync(PunchoutBasketTransferContext context, CancellationToken cancellationToken)
{
var requestUri = $"https://your-punchout-service-provider/basket/{context.PunchoutSessionId}";
var requestContent = JsonSerializer.Serialize(new TransferBasketRequest(BasketConverter.Convert(context.Basket)), jsonSerializerOptions);
using var httpClient = new HttpClient();
var response = await httpClient.PostAsync(
requestUri,
new StringContent(requestContent, Encoding.UTF8, "application/json"),
cancellationToken);
await ValidateHttpResponseAsync(response, requestUri, requestContent, cancellationToken);
var responseContent = await response.Content.ReadAsStringAsync(cancellationToken);
var transferBasketResponse = JsonSerializer.Deserialize<TransferBasketResponse>(responseContent, jsonSerializerOptions)!;
return PunchoutNextAction.HttpPost(transferBasketResponse.Url, transferBasketResponse.Data);
}
record TransferBasketRequest([property: JsonPropertyName("basket")] Basket Basket);
record Basket(
[property: JsonPropertyName("currencyId")] string CurrencyId,
[property: JsonPropertyName("languageId")] int LanguageId,
[property: JsonPropertyName("pricesInclTax")] bool PricesInclTax,
[property: JsonPropertyName("shopAccountEmail")] string ShopAccountEmail,
[property: JsonPropertyName("accountId")] string AccountId,
[property: JsonPropertyName("accountType"), JsonConverter(typeof(JsonStringEnumConverter))] AccountType AccountType,
[property: JsonPropertyName("contactId")] string? ContactId,
[property: JsonPropertyName("totalExclDiscount")] decimal TotalExclDiscount,
[property: JsonPropertyName("totalExclTax")] decimal TotalExclTax,
[property: JsonPropertyName("totalInclTax")] decimal TotalInclTax,
[property: JsonPropertyName("invoiceDiscount")] decimal InvoiceDiscount,
[property: JsonPropertyName("taxAmount")] decimal TaxAmount,
[property: JsonPropertyName("taxPercent")] decimal TaxPercent,
[property: JsonPropertyName("roundOff")] decimal RoundOff,
[property: JsonPropertyName("paymentDiscount")] decimal PaymentDiscount,
[property: JsonPropertyName("salesAgreementId")] string? SalesAgreementId,
[property: JsonPropertyName("shippingCost")] ShippingCost? ShippingCost,
[property: JsonPropertyName("lines")] IReadOnlyCollection<BasketLine> Lines,
[property: JsonPropertyName("serviceCosts")] IReadOnlyCollection<ServiceCost>? ServiceCosts,
[property: JsonPropertyName("taxLines")] IReadOnlyCollection<TaxLine>? TaxLines,
[property: JsonPropertyName("extraFields"), JsonConverter(typeof(ExtraFieldsJsonConverter))] IReadOnlyDictionary<string, object?>? ExtraFields);
record BasketLine(
[property: JsonPropertyName("product")] BasketLineProduct Product,
[property: JsonPropertyName("quantity")] decimal Quantity,
[property: JsonPropertyName("price")] decimal Price,
[property: JsonPropertyName("taxPercent")] decimal TaxPercent,
[property: JsonPropertyName("discountAmount")] decimal DiscountAmount,
[property: JsonPropertyName("discountPercent")] decimal DiscountPercent,
[property: JsonPropertyName("subTotal")] decimal SubTotal,
[property: JsonPropertyName("isReadOnly")] bool IsReadOnly,
[property: JsonPropertyName("isSupplementary")] bool IsSupplementary,
[property: JsonPropertyName("salesAgreementLineId")] string? SalesAgreementLineId,
[property: JsonPropertyName("configurationId")] long? ConfigurationId,
[property: JsonPropertyName("extendedTexts")] IReadOnlyCollection<ExtendedText>? ExtendedTexts,
[property: JsonPropertyName("serviceCosts")] IReadOnlyCollection<ServiceCost>? ServiceCosts);
record BasketLineProduct(
[property: JsonPropertyName("id")] string Id,
[property: JsonPropertyName("title")] string Title,
[property: JsonPropertyName("description")] string? Description,
[property: JsonPropertyName("categoryIds")] IReadOnlyCollection<string> CategoryIds,
[property: JsonPropertyName("variant")] BasketLineVariant? Variant,
[property: JsonPropertyName("unitOfMeasure")] UnitOfMeasure? UnitOfMeasure,
[property: JsonPropertyName("extraFields"), JsonConverter(typeof(ExtraFieldsJsonConverter))] IReadOnlyDictionary<string, object?>? ExtraFields);
record BasketLineVariant(
[property: JsonPropertyName("id")] string Id,
[property: JsonPropertyName("title")] string Title);
record CustomerAddress(
[property: JsonPropertyName("address")] string? Address,
[property: JsonPropertyName("address2")] string? Address2,
[property: JsonPropertyName("zipCode")] string? ZipCode,
[property: JsonPropertyName("city")] string? City,
[property: JsonPropertyName("countryId")] string CountryId,
[property: JsonPropertyName("state")] string? State,
[property: JsonPropertyName("phoneNo")] string? PhoneNo,
[property: JsonPropertyName("name")] string? Name,
[property: JsonPropertyName("contactName")] string? ContactName,
[property: JsonPropertyName("extraFields"), JsonConverter(typeof(ExtraFieldsJsonConverter))] IReadOnlyDictionary<string, object?>? ExtraFields);
record TaxLine(
[property: JsonPropertyName("id")] string Id,
[property: JsonPropertyName("description")] string? Description,
[property: JsonPropertyName("amount")] decimal Amount,
[property: JsonPropertyName("percent")] decimal Percent);
record ServiceCost(
[property: JsonPropertyName("id")] string? Id,
[property: JsonPropertyName("title")] string? Title,
[property: JsonPropertyName("price")] decimal Price,
[property: JsonPropertyName("subTotal")] decimal SubTotal,
[property: JsonPropertyName("quantity")] decimal Quantity);
record ExtendedText([property: JsonPropertyName("title")] string Title);
record TransferBasketResponse(
[property: JsonPropertyName("url")] string Url,
[property: JsonPropertyName("data")] IReadOnlyDictionary<string, string> Data);
OnImportOrderRequestReceivedAsync
Handles import order request. Authorization must be verified within this method.
Sana calls this method and passes the instance of PunchoutImportOrderRequestContext with information needed to handle the import order request.
Example:
public override async Task OnImportOrderRequestReceivedAsync(PunchoutImportOrderRequestContext context, CancellationToken cancellationToken)
{
if (!IsRequestAuthorized(context.Request))
{
context.Response.StatusCode = 401;
return;
}
using var reader = new StreamReader(context.Request.Body, Encoding.UTF8);
var requestBody = await reader.ReadToEndAsync();
var orderInput = OrderInputConverter.Convert(JsonSerializer.Deserialize<OrderInput>(requestBody, jsonSerializerOptions)!);
var order = await context.ImportOrderAsync(orderInput);
var responseContent = JsonSerializer.Serialize(OrderConverter.Convert(order), jsonSerializerOptions);
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(responseContent, Encoding.UTF8, cancellationToken);
}
bool IsRequestAuthorized(IHttpRequest request)
{
if (string.IsNullOrEmpty(Configuration.ApiAuthorizationKey))
return false;
return Configuration.ApiAuthorizationKey == request.Headers["Authorization"].ToString();
}
record OrderInput(
[property: JsonPropertyName("currencyId")] string? CurrencyId,
[property: JsonPropertyName("shopAccountEmail")] string? ShopAccountEmail,
[property: JsonPropertyName("accountId")] string? AccountId,
[property: JsonPropertyName("accountType"), JsonConverter(typeof(JsonStringEnumConverter))] AccountType AccountType,
[property: JsonPropertyName("contactId")] string? ContactId,
[property: JsonPropertyName("referenceNo")] string? ReferenceNo,
[property: JsonPropertyName("requestedDeliveryDate")] DateOnly? RequestedDeliveryDate,
[property: JsonPropertyName("comment")] string? Comment,
[property: JsonPropertyName("salesAgreementId")] string? SalesAgreementId,
[property: JsonPropertyName("shippingAddress")] ShippingAddressInput? ShippingAddress,
[property: JsonPropertyName("lines")] IReadOnlyCollection<OrderLineInput>? Lines,
[property: JsonPropertyName("extraFields"), JsonConverter(typeof(ExtraFieldsJsonConverter))] IReadOnlyDictionary<string, object?>? ExtraFields);
record OrderLineInput(
[property: JsonPropertyName("productId")] string? ProductId,
[property: JsonPropertyName("variantId")] string? VariantId,
[property: JsonPropertyName("unitOfMeasureId")] string? UnitOfMeasureId,
[property: JsonPropertyName("quantity")] decimal Quantity,
[property: JsonPropertyName("salesAgreementLineId")] string? SalesAgreementLineId,
[property: JsonPropertyName("configurationId")] long? ConfigurationId);
record ShippingAddressInput(
[property: JsonPropertyName("id")] string? Id,
[property: JsonPropertyName("address")] string? Address,
[property: JsonPropertyName("address2")] string? Address2,
[property: JsonPropertyName("zipCode")] string? ZipCode,
[property: JsonPropertyName("city")] string? City,
[property: JsonPropertyName("countryId")] string? CountryId,
[property: JsonPropertyName("state")] string? State,
[property: JsonPropertyName("phoneNo")] string? PhoneNo,
[property: JsonPropertyName("name")] string? Name,
[property: JsonPropertyName("contactName")] string? ContactName,
[property: JsonPropertyName("extraFields"), JsonConverter(typeof(ExtraFieldsJsonConverter))] IReadOnlyDictionary<string, object?>? ExtraFields);
OnInvoicesRequestReceivedAsync
Handles invoices request. Authorization must be verified within this method.
Sana calls this method and passes the instance of PunchoutInvoicesRequestContext with information needed to handle the invoices request.
Example:
public override async Task OnInvoicesRequestReceivedAsync(PunchoutInvoicesRequestContext context, CancellationToken cancellationToken)
{
if (!IsRequestAuthorized(context.Request))
{
context.Response.StatusCode = 401;
return;
}
var orderId = Uri.UnescapeDataString(context.Request.Path.Split('/').Last());
var invoices = await context.GetInvoicesAsync(orderId);
var responseContent = JsonSerializer.Serialize(invoices.Select(InvoiceConverter.Convert), jsonSerializerOptions);
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(responseContent, Encoding.UTF8, cancellationToken);
}
record Invoice(
[property: JsonPropertyName("documentId")] string DocumentId,
[property: JsonPropertyName("originalOrderId")] string OriginalOrderId,
[property: JsonPropertyName("currencyId")] string CurrencyId,
[property: JsonPropertyName("pricesInclTax")] bool PricesInclTax,
[property: JsonPropertyName("accountId")] string AccountId,
[property: JsonPropertyName("accountType"), JsonConverter(typeof(JsonStringEnumConverter))] AccountType AccountType,
[property: JsonPropertyName("contactId")] string? ContactId,
[property: JsonPropertyName("contactName")] string? ContactName,
[property: JsonPropertyName("totalExclDiscount")] decimal TotalExclDiscount,
[property: JsonPropertyName("totalExclTax")] decimal TotalExclTax,
[property: JsonPropertyName("totalInclTax")] decimal TotalInclTax,
[property: JsonPropertyName("invoiceDiscount")] decimal InvoiceDiscount,
[property: JsonPropertyName("taxAmount")] decimal TaxAmount,
[property: JsonPropertyName("taxPercent")] decimal TaxPercent,
[property: JsonPropertyName("roundOff")] decimal RoundOff,
[property: JsonPropertyName("paymentDiscount")] decimal PaymentDiscount,
[property: JsonPropertyName("paymentDiscountDate")] DateOnly? PaymentDiscountDate,
[property: JsonPropertyName("salesAgreementId")] string? SalesAgreementId,
[property: JsonPropertyName("outstandingAmount")] decimal OutstandingAmount,
[property: JsonPropertyName("comment")] string? Comment,
[property: JsonPropertyName("postingDate")] DateOnly? PostingDate,
[property: JsonPropertyName("orderDate")] DateOnly OrderDate,
[property: JsonPropertyName("documentDate")] DateOnly? DocumentDate,
[property: JsonPropertyName("dueDate")] DateOnly? DueDate,
[property: JsonPropertyName("shipmentDate")] DateOnly? ShipmentDate,
[property: JsonPropertyName("shippingStatus")] string? ShippingStatus,
[property: JsonPropertyName("shippingMethodCode")] string? ShippingMethodCode,
[property: JsonPropertyName("shippingMethodName")] string? ShippingMethodName,
[property: JsonPropertyName("shippingTrackingLink")] string? ShippingTrackingLink,
[property: JsonPropertyName("shippingTrackingNumber")] string? ShippingTrackingNumber,
[property: JsonPropertyName("locationCode")] string? LocationCode,
[property: JsonPropertyName("locationTitle")] string? LocationTitle,
[property: JsonPropertyName("paymentStatus")] string? PaymentStatus,
[property: JsonPropertyName("paymentMethodCode")] string? PaymentMethodCode,
[property: JsonPropertyName("paymentMethodName")] string? PaymentMethodName,
[property: JsonPropertyName("paymentTermsCode")] string? PaymentTermsCode,
[property: JsonPropertyName("paymentTermsDescription")] string? PaymentTermsDescription,
[property: JsonPropertyName("billingAddress")] CustomerAddress BillingAddress,
[property: JsonPropertyName("shippingAddress")] CustomerAddress ShippingAddress,
[property: JsonPropertyName("sellToAddress")] CustomerAddress? SellToAddress,
[property: JsonPropertyName("payerAddress")] CustomerAddress? PayerAddress,
[property: JsonPropertyName("shippingCost")] ShippingCost? ShippingCost,
[property: JsonPropertyName("lines")] IReadOnlyCollection<InvoiceLine> Lines,
[property: JsonPropertyName("serviceCosts")] IReadOnlyCollection<ServiceCost>? ServiceCosts,
[property: JsonPropertyName("taxLines")] IReadOnlyCollection<TaxLine>? TaxLines,
[property: JsonPropertyName("extraFields"), JsonConverter(typeof(ExtraFieldsJsonConverter))] IReadOnlyDictionary<string, object?>? ExtraFields);
record InvoiceLine(
[property: JsonPropertyName("product")] DocumentLineProduct Product,
[property: JsonPropertyName("quantity")] decimal Quantity,
[property: JsonPropertyName("quantityOutstanding")] decimal QuantityOutstanding,
[property: JsonPropertyName("price")] decimal Price,
[property: JsonPropertyName("taxPercent")] decimal TaxPercent,
[property: JsonPropertyName("discountAmount")] decimal DiscountAmount,
[property: JsonPropertyName("discountPercent")] decimal DiscountPercent,
[property: JsonPropertyName("subTotal")] decimal SubTotal,
[property: JsonPropertyName("isCancelled")] bool IsCancelled,
[property: JsonPropertyName("isSupplementary")] bool IsSupplementary,
[property: JsonPropertyName("shippingStatus")] string? ShippingStatus,
[property: JsonPropertyName("shipmentDate")] DateOnly? ShipmentDate,
[property: JsonPropertyName("shopAccountEmail")] string? ShopAccountEmail,
[property: JsonPropertyName("comment")] string? Comment,
[property: JsonPropertyName("originalOrderId")] string? OriginalOrderId,
[property: JsonPropertyName("salesAgreementId")] string? SalesAgreementId,
[property: JsonPropertyName("salesAgreementLineId")] string? SalesAgreementLineId,
[property: JsonPropertyName("configurationId")] long? ConfigurationId,
[property: JsonPropertyName("extendedTexts")] IReadOnlyCollection<ExtendedText>? ExtendedTexts,
[property: JsonPropertyName("serviceCosts")] IReadOnlyCollection<ServiceCost>? ServiceCosts);
OnShipmentsRequestReceivedAsync
Handles shipments request. Authorization must be verified within this method.
Sana calls this method and passes the instance of PunchoutShipmentsRequestContext with information needed to handle the shipments request.
Example:
public override async Task OnShipmentsRequestReceivedAsync(PunchoutShipmentsRequestContext context, CancellationToken cancellationToken)
{
if (!IsRequestAuthorized(context.Request))
{
context.Response.StatusCode = 401;
return;
}
var orderId = Uri.UnescapeDataString(context.Request.Path.Split('/').Last());
var shipments = await context.GetShipmentsAsync(orderId);
var responseContent = JsonSerializer.Serialize(shipments.Select(ShipmentConverter.Convert), jsonSerializerOptions);
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(responseContent, Encoding.UTF8, cancellationToken);
}
record Shipment(
[property: JsonPropertyName("documentId")] string DocumentId,
[property: JsonPropertyName("originalOrderId")] string OriginalOrderId,
[property: JsonPropertyName("currencyId")] string CurrencyId,
[property: JsonPropertyName("pricesInclTax")] bool PricesInclTax,
[property: JsonPropertyName("accountId")] string AccountId,
[property: JsonPropertyName("accountType"), JsonConverter(typeof(JsonStringEnumConverter))] AccountType AccountType,
[property: JsonPropertyName("contactId")] string? ContactId,
[property: JsonPropertyName("contactName")] string? ContactName,
[property: JsonPropertyName("totalExclDiscount")] decimal TotalExclDiscount,
[property: JsonPropertyName("totalExclTax")] decimal TotalExclTax,
[property: JsonPropertyName("totalInclTax")] decimal TotalInclTax,
[property: JsonPropertyName("invoiceDiscount")] decimal InvoiceDiscount,
[property: JsonPropertyName("taxAmount")] decimal TaxAmount,
[property: JsonPropertyName("taxPercent")] decimal TaxPercent,
[property: JsonPropertyName("roundOff")] decimal RoundOff,
[property: JsonPropertyName("paymentDiscount")] decimal PaymentDiscount,
[property: JsonPropertyName("paymentDiscountDate")] DateOnly? PaymentDiscountDate,
[property: JsonPropertyName("salesAgreementId")] string? SalesAgreementId,
[property: JsonPropertyName("comment")] string? Comment,
[property: JsonPropertyName("postingDate")] DateOnly? PostingDate,
[property: JsonPropertyName("orderDate")] DateOnly OrderDate,
[property: JsonPropertyName("documentDate")] DateOnly? DocumentDate,
[property: JsonPropertyName("shipmentDate")] DateOnly? ShipmentDate,
[property: JsonPropertyName("shippingStatus")] string? ShippingStatus,
[property: JsonPropertyName("shippingMethodCode")] string? ShippingMethodCode,
[property: JsonPropertyName("shippingMethodName")] string? ShippingMethodName,
[property: JsonPropertyName("shippingTrackingLink")] string? ShippingTrackingLink,
[property: JsonPropertyName("shippingTrackingNumber")] string? ShippingTrackingNumber,
[property: JsonPropertyName("locationCode")] string? LocationCode,
[property: JsonPropertyName("locationTitle")] string? LocationTitle,
[property: JsonPropertyName("paymentStatus")] string? PaymentStatus,
[property: JsonPropertyName("paymentMethodCode")] string? PaymentMethodCode,
[property: JsonPropertyName("paymentMethodName")] string? PaymentMethodName,
[property: JsonPropertyName("paymentTermsCode")] string? PaymentTermsCode,
[property: JsonPropertyName("paymentTermsDescription")] string? PaymentTermsDescription,
[property: JsonPropertyName("billingAddress")] CustomerAddress BillingAddress,
[property: JsonPropertyName("shippingAddress")] CustomerAddress ShippingAddress,
[property: JsonPropertyName("sellToAddress")] CustomerAddress? SellToAddress,
[property: JsonPropertyName("payerAddress")] CustomerAddress? PayerAddress,
[property: JsonPropertyName("shippingCost")] ShippingCost? ShippingCost,
[property: JsonPropertyName("lines")] IReadOnlyCollection<ShipmentLine> Lines,
[property: JsonPropertyName("serviceCosts")] IReadOnlyCollection<ServiceCost>? ServiceCosts,
[property: JsonPropertyName("taxLines")] IReadOnlyCollection<TaxLine>? TaxLines,
[property: JsonPropertyName("extraFields"), JsonConverter(typeof(ExtraFieldsJsonConverter))] IReadOnlyDictionary<string, object?>? ExtraFields);
record ShipmentLine(
[property: JsonPropertyName("product")] DocumentLineProduct Product,
[property: JsonPropertyName("quantity")] decimal Quantity,
[property: JsonPropertyName("quantityOutstanding")] decimal QuantityOutstanding,
[property: JsonPropertyName("price")] decimal Price,
[property: JsonPropertyName("taxPercent")] decimal TaxPercent,
[property: JsonPropertyName("discountAmount")] decimal DiscountAmount,
[property: JsonPropertyName("discountPercent")] decimal DiscountPercent,
[property: JsonPropertyName("subTotal")] decimal SubTotal,
[property: JsonPropertyName("isCancelled")] bool IsCancelled,
[property: JsonPropertyName("isSupplementary")] bool IsSupplementary,
[property: JsonPropertyName("shippingStatus")] string? ShippingStatus,
[property: JsonPropertyName("shipmentDate")] DateOnly? ShipmentDate,
[property: JsonPropertyName("comment")] string? Comment,
[property: JsonPropertyName("salesAgreementId")] string? SalesAgreementId,
[property: JsonPropertyName("salesAgreementLineId")] string? SalesAgreementLineId,
[property: JsonPropertyName("configurationId")] long? ConfigurationId,
[property: JsonPropertyName("extendedTexts")] IReadOnlyCollection<ExtendedText>? ExtendedTexts,
[property: JsonPropertyName("serviceCosts")] IReadOnlyCollection<ServiceCost>? ServiceCosts);
OnCreditNotesRequestReceivedAsync
Handles credit notes request. Authorization must be verified within this method.
Sana calls this method and passes the instance of PunchoutCreditNotesRequestContext with information needed to handle the credit notes request.
Example:
public override async Task OnCreditNotesRequestReceivedAsync(PunchoutCreditNotesRequestContext context, CancellationToken cancellationToken)
{
if (!IsRequestAuthorized(context.Request))
{
context.Response.StatusCode = 401;
return;
}
var orderId = Uri.UnescapeDataString(context.Request.Path.Split('/').Last());
var creditNotes = await context.GetCreditNotesAsync(orderId);
var responseContent = JsonSerializer.Serialize(creditNotes.Select(CreditNoteConverter.Convert), jsonSerializerOptions);
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(responseContent, Encoding.UTF8, cancellationToken);
}
record CreditNote(
[property: JsonPropertyName("documentId")] string DocumentId,
[property: JsonPropertyName("originalOrderId")] string OriginalOrderId,
[property: JsonPropertyName("originalInvoiceId")] string? OriginalInvoiceId,
[property: JsonPropertyName("currencyId")] string CurrencyId,
[property: JsonPropertyName("pricesInclTax")] bool PricesInclTax,
[property: JsonPropertyName("accountId")] string AccountId,
[property: JsonPropertyName("accountType"), JsonConverter(typeof(JsonStringEnumConverter))] AccountType AccountType,
[property: JsonPropertyName("contactId")] string? ContactId,
[property: JsonPropertyName("contactName")] string? ContactName,
[property: JsonPropertyName("totalExclDiscount")] decimal TotalExclDiscount,
[property: JsonPropertyName("totalExclTax")] decimal TotalExclTax,
[property: JsonPropertyName("totalInclTax")] decimal TotalInclTax,
[property: JsonPropertyName("invoiceDiscount")] decimal InvoiceDiscount,
[property: JsonPropertyName("taxAmount")] decimal TaxAmount,
[property: JsonPropertyName("taxPercent")] decimal TaxPercent,
[property: JsonPropertyName("roundOff")] decimal RoundOff,
[property: JsonPropertyName("paymentDiscount")] decimal PaymentDiscount,
[property: JsonPropertyName("paymentDiscountDate")] DateOnly? PaymentDiscountDate,
[property: JsonPropertyName("salesAgreementId")] string? SalesAgreementId,
[property: JsonPropertyName("outstandingAmount")] decimal OutstandingAmount,
[property: JsonPropertyName("comment")] string? Comment,
[property: JsonPropertyName("postingDate")] DateOnly? PostingDate,
[property: JsonPropertyName("orderDate")] DateOnly OrderDate,
[property: JsonPropertyName("documentDate")] DateOnly? DocumentDate,
[property: JsonPropertyName("shipmentDate")] DateOnly? ShipmentDate,
[property: JsonPropertyName("shippingStatus")] string? ShippingStatus,
[property: JsonPropertyName("shippingMethodCode")] string? ShippingMethodCode,
[property: JsonPropertyName("shippingMethodName")] string? ShippingMethodName,
[property: JsonPropertyName("shippingTrackingLink")] string? ShippingTrackingLink,
[property: JsonPropertyName("shippingTrackingNumber")] string? ShippingTrackingNumber,
[property: JsonPropertyName("locationCode")] string? LocationCode,
[property: JsonPropertyName("locationTitle")] string? LocationTitle,
[property: JsonPropertyName("paymentStatus")] string? PaymentStatus,
[property: JsonPropertyName("paymentMethodCode")] string? PaymentMethodCode,
[property: JsonPropertyName("paymentMethodName")] string? PaymentMethodName,
[property: JsonPropertyName("paymentTermsCode")] string? PaymentTermsCode,
[property: JsonPropertyName("paymentTermsDescription")] string? PaymentTermsDescription,
[property: JsonPropertyName("billingAddress")] CustomerAddress BillingAddress,
[property: JsonPropertyName("shippingAddress")] CustomerAddress ShippingAddress,
[property: JsonPropertyName("sellToAddress")] CustomerAddress? SellToAddress,
[property: JsonPropertyName("payerAddress")] CustomerAddress? PayerAddress,
[property: JsonPropertyName("shippingCost")] ShippingCost? ShippingCost,
[property: JsonPropertyName("lines")] IReadOnlyCollection<CreditNoteLine> Lines,
[property: JsonPropertyName("serviceCosts")] IReadOnlyCollection<ServiceCost>? ServiceCosts,
[property: JsonPropertyName("taxLines")] IReadOnlyCollection<TaxLine>? TaxLines,
[property: JsonPropertyName("extraFields"), JsonConverter(typeof(ExtraFieldsJsonConverter))] IReadOnlyDictionary<string, object?>? ExtraFields);
record CreditNoteLine(
[property: JsonPropertyName("product")] DocumentLineProduct Product,
[property: JsonPropertyName("quantity")] decimal Quantity,
[property: JsonPropertyName("quantityOutstanding")] decimal QuantityOutstanding,
[property: JsonPropertyName("price")] decimal Price,
[property: JsonPropertyName("taxPercent")] decimal TaxPercent,
[property: JsonPropertyName("discountAmount")] decimal DiscountAmount,
[property: JsonPropertyName("discountPercent")] decimal DiscountPercent,
[property: JsonPropertyName("subTotal")] decimal SubTotal,
[property: JsonPropertyName("isCancelled")] bool IsCancelled,
[property: JsonPropertyName("isSupplementary")] bool IsSupplementary,
[property: JsonPropertyName("shippingStatus")] string? ShippingStatus,
[property: JsonPropertyName("shipmentDate")] DateOnly? ShipmentDate,
[property: JsonPropertyName("originalInvoiceId")] string? OriginalInvoiceId,
[property: JsonPropertyName("comment")] string? Comment,
[property: JsonPropertyName("salesAgreementId")] string? SalesAgreementId,
[property: JsonPropertyName("salesAgreementLineId")] string? SalesAgreementLineId,
[property: JsonPropertyName("configurationId")] long? ConfigurationId,
[property: JsonPropertyName("extendedTexts")] IReadOnlyCollection<ExtendedText>? ExtendedTexts,
[property: JsonPropertyName("serviceCosts")] IReadOnlyCollection<ServiceCost>? ServiceCosts);
OnProductsRequestReceivedAsync
Handles products request. Authorization should be verified in scope of this method.
Sana calls this method and passes the instance of PunchoutProductsRequestContext with information needed to handle the products request.
Example:
public override async Task OnProductsRequestReceivedAsync(PunchoutProductsRequestContext context, CancellationToken cancellationToken)
{
if (!IsRequestAuthorized(context.Request))
{
context.Response.StatusCode = 401;
return;
}
var products = await context.GetProductsAsync(ParseProductsLoadOptions(context.Request.Query));
var responseContent = JsonSerializer.Serialize(ProductCollectionConverter.Convert(products));
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(responseContent, Encoding.UTF8, cancellationToken);
}
record ProductCollection(
[property: JsonPropertyName("items")] IReadOnlyCollection<Product> Items,
[property: JsonPropertyName("totalCount")] int TotalCount);
record Product(
[property: JsonPropertyName("id")] string Id,
[property: JsonPropertyName("title")] string Title,
[property: JsonPropertyName("description")] string? Description,
[property: JsonPropertyName("categoryIds")] IReadOnlyCollection<string> CategoryIds,
[property: JsonPropertyName("unitOfMeasureId")] string? UnitOfMeasureId,
[property: JsonPropertyName("image")] ProductImage? Image,
[property: JsonPropertyName("isCustomerSpecific")] bool IsCustomerSpecific,
[property: JsonPropertyName("extraFields"), JsonConverter(typeof(ExtraFieldsJsonConverter))] IReadOnlyDictionary<string, object?>? ExtraFields);
record ProductImage(
[property: JsonPropertyName("smallImageUrl")] string SmallImageUrl,
[property: JsonPropertyName("mediumImageUrl")] string? MediumImageUrl,
[property: JsonPropertyName("largeImageUrl")] string? LargeImageUrl);
OnShippingAddressesRequestReceivedAsync
Handles shipping addresses request. Authorization must be verified within this method.
Sana calls this method and passes the instance of PunchoutShippingAddressesRequestContext with information needed to handle the shipping addresses request.
Example:
public override async Task OnShippingAddressesRequestReceivedAsync(PunchoutShippingAddressesRequestContext context, CancellationToken cancellationToken)
{
if (!IsRequestAuthorized(context.Request))
{
context.Response.StatusCode = 401;
return;
}
var customerId = Uri.UnescapeDataString(context.Request.Path.Split('/').Last());
var shippingAddresses = await context.GetShippingAddressesAsync(customerId);
var responseContent = JsonSerializer.Serialize(shippingAddresses.Select(ShippingAddressConverter.Convert));
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(responseContent, Encoding.UTF8, cancellationToken);
}
record ShippingAddress(
[property: JsonPropertyName("id")] string Id,
[property: JsonPropertyName("address")] string? Address,
[property: JsonPropertyName("address2")] string? Address2,
[property: JsonPropertyName("zipCode")] string? ZipCode,
[property: JsonPropertyName("city")] string? City,
[property: JsonPropertyName("countryId")] string? CountryId,
[property: JsonPropertyName("state")] string? State,
[property: JsonPropertyName("phoneNo")] string? PhoneNo,
[property: JsonPropertyName("name")] string? Name,
[property: JsonPropertyName("contactName")] string? ContactName,
[property: JsonPropertyName("extraFields"), JsonConverter(typeof(ExtraFieldsJsonConverter))] IReadOnlyDictionary<string, object?>? ExtraFields);
See also
- PunchoutLoginRequestContext reference
- PunchoutLoginInfo reference
- PunchoutBasketTransferContext reference
- PunchoutNextAction reference
- PunchoutImportOrderRequestContext reference
- PunchoutInvoicesRequestContext reference
- PunchoutShipmentsRequestContext reference
- PunchoutCreditNotesRequestContext reference
- PunchoutProductsRequestContext reference
- PunchoutShippingAddressesRequestContext reference