@smartsoft001/paypal

The PayPal end of a transaction: four calls through the legacy REST SDK, with the credentials chosen per transaction rather than once at startup.


Install

npm install @smartsoft001/paypal @smartsoft001/trans-domain paypal-rest-sdk

The manifest declares @smartsoft001/trans-domain as a peer dependency, pinned to its own version. Everything else the service reaches for has to be installed alongside it. paypal-rest-sdk carries every outbound call, @nestjs/common provides @Injectable and Logger, and @nestjs/core provides ModuleRef. The three imports from @smartsoft001/trans-domain are used only as types, so they cost nothing at runtime but are needed to compile. There is no HTTP client here, because all four methods go through the SDK.

What it is

One of the four payment providers behind @smartsoft001/trans-shell-app-services. The service implements ITransPaymentSingleService, the contract that the transaction domain calls whenever a transaction names paypal as its system, and it adds one method the contract does not declare: confirm, which the PayPal controller in @smartsoft001/trans-shell-nestjs calls when a buyer comes back from the PayPal approval page.

The package ships no NestJS module and no forRoot. An application normally gets the service from TransShellNestjsModule.forRoot({ paypalConfig }), which registers PaypalConfig as a value provider and PaypalService as a class provider, and only when that key is present. Register the two providers by hand, as the example does, to use the PayPal calls without the rest of the transaction shell.

Credentials are never installed globally. paypal.configure() is never called; instead every SDK call receives its own environment object built from the config that was resolved for that transaction. That is what makes the second configuration path work: a provider registered under PAYPAL_CONFIG_PROVIDER can hand back a different merchant account per transaction, and nothing about the service is shared between calls.

Usage

import { Module, Provider } from '@nestjs/common';

import { PaypalConfig, PaypalService } from '@smartsoft001/paypal';

/**
 * `PaypalService` implements `ITransPaymentSingleService` from
 * `@smartsoft001/trans-domain`, so an application normally gets it from
 * `TransShellNestjsModule.forRoot({ paypalConfig })`, which registers the
 * service only when that key is present. Register the two providers by hand
 * when you want the PayPal calls without the rest of the transaction shell.
 *
 * `PaypalConfig` is the one config class in this family with a positional
 * constructor, so build it with `new` rather than an object literal. The
 * argument order is clientId, clientSecret, currencyCode, returnUrl, apiUrl,
 * cancelUrl, test.
 *
 * Credentials travel with every SDK call, so nothing is sent while the
 * provider is being registered.
 */
export const paypalProviders: Provider[] = [
  PaypalService,
  {
    provide: PaypalConfig,
    useValue: new PaypalConfig(
      'paypal-client-id',
      'paypal-client-secret',
      // Currency of every item and total the service builds.
      'PLN',
      // `returnUrl` is kept for callers; the service builds the PayPal return
      // url from `apiUrl` below and ignores this field today.
      'https://app.example.com/orders/thank-you',
      // `apiUrl` is your own API base, not a PayPal host: the service appends
      // `paypal/{id}/confirm` to it, which is the route the PayPal controller
      // in `@smartsoft001/trans-shell-nestjs` serves. Keep the trailing slash.
      'https://api.example.com/',
      // Where PayPal sends a buyer who abandons the payment.
      'https://app.example.com/orders/canceled',
      // `test: true` runs the SDK in `sandbox` mode, `false` in `live`.
      true,
    ),
  },
];

/**
 * The module needs no HTTP client, because this package routes every call
 * through `paypal-rest-sdk`. The second dependency, `ModuleRef`, comes from
 * Nest itself: `PaypalService` uses it to look up an optional
 * `IPaypalConfigProvider` under the `PAYPAL_CONFIG_PROVIDER` token and falls
 * back to the config above when there is none.
 */
@Module({
  providers: paypalProviders,
  exports: [PaypalService],
})
export class PaypalPaymentsModule {}

The region registers the two providers the service needs and wraps them in a module that imports nothing at all. PaypalConfig is the one config class in this family with a positional constructor, so it is built with new rather than passed as an object literal. The comments on the arguments carry the part that is easy to get wrong: apiUrl is your own API base, not a PayPal host.

Its spec compiles that module and asserts three things. PaypalService resolves, so the two constructor dependencies are satisfiable from these providers alone. The PaypalConfig read back out of the injector equals the one the example built, so the value provider is the injectable rather than a copy. And every mocked SDK entry point, including configure, is still untouched after the service has been resolved, which is the offline guarantee stated above: nothing is sent while the provider is being registered.

API

PaypalConfig

A plain class with a positional constructor, used as both the injection token and the type. new PaypalConfig(clientId, clientSecret, currencyCode, returnUrl, apiUrl, cancelUrl, test?).

PositionFieldTypeWhat it does
1clientIdstringSent as client_id with every SDK call.
2clientSecretstringSent as client_secret with every SDK call.
3currencyCodestringCurrency of the item, the total and the refund amount.
4returnUrlstringNot read by this service, which builds its return url from apiUrl instead. The PayPal controller in the transaction shell redirects a buyer there once the payment is confirmed.
5apiUrlstringYour own API base. create sends return_url as apiUrl plus paypal/{id}/confirm, which is the route the PayPal controller serves, so keep the trailing slash.
6cancelUrlstringSent as cancel_url, where PayPal sends a buyer who abandons the payment.
7testbooleantrue runs every call in sandbox mode, false or absent in live. Nothing else changes.

PaypalService

MethodReturnsWhat it does
create(obj)Promise<{ orderId: string; redirectUrl: string }>Builds a one-item sale payment with price and total as obj.amount / 100, then returns the payment id and the approval_url link the buyer is sent to.
confirm(payerId, paymentId, amount, externalData)Promise<any>Executes an approved payment. Not part of the shared contract. amount is passed through as the total, already in major units.
getStatus<T>(trans)Promise<{ status: TransStatus; data: any }>Reads the payment by the order id stored on the started history entry and maps its state.
refund(trans, comment)Promise<any>Refunds trans.amount / 100 with comment as the description. Throws Paypal transaction ID not found for refund when no sale id can be found on the history.

create takes { id, name, amount, firstName?, lastName?, email?, contactPhone?, clientIp, data, options? }, which is the shared ITransPaymentSingleService shape with options made optional. Nothing here reads options, so a value passed by a caller is accepted and ignored, while PayU and Paynow read it.

refund does not use the order id it looks up. It walks the history for a completed entry carrying customData.transactions[0].related_resources[0].sale.id and refunds that sale, so a transaction that was never confirmed through this service cannot be refunded through it either.

Choosing the credentials

PathHow it is registeredWhen it wins
Static configA PaypalConfig value provider, as in the example.Whenever no config provider resolves.
Per transactionA class implementing IPaypalConfigProvider under PAYPAL_CONFIG_PROVIDER.Whenever the lookup succeeds. Its get(data) receives the transaction's data.

PAYPAL_CONFIG_PROVIDER is a string constant and IPaypalConfigProvider is an abstract class with a single get(data: any): Promise<PaypalConfig>. Every public method resolves the config first, through moduleRef.get(PAYPAL_CONFIG_PROVIDER, { strict: false }) inside a try. The non-strict lookup means the provider can live in any module of the application. A missing token throws, the catch logs PayPal config provider not found at warning level, and the statically injected config is used instead, so an application on the static path logs that warning on every call.

External calls

MethodSDK callEnvironment
createpaypal.payment.create{ mode: test ? 'sandbox' : 'live', client_id, client_secret }
confirmpaypal.payment.executethe same, rebuilt per call
getStatuspaypal.payment.getthe same, rebuilt per call
refundpaypal.sale.refundthe same, rebuilt per call

There is no base url to configure. The private helper that returns the sandbox and live REST hosts is dead code, kept from an earlier HTTP implementation.

Status mapping

getStatus uppercases the PayPal state before matching it.

PayPal stateTransStatus
COMPLETEDcompleted
APPROVEDcompleted
CREATEDpending
SAVEDpending
VOIDEDcanceled
anything elsethe uppercased state, returned unchanged

The default branch returns a value that is not a TransStatus, so a state PayPal adds later reaches the domain as itself rather than as an error.