FRAGMENT
C Make Payments#

FRAGMENT enables you to make payments that automatically reconcile to your Ledger.

card_payin
initiated
settled
Payment

a. Payment Type

#

A Payment Type is a reusable definition of one type of money movement in your Ledger. Define them in your Schema.

Payment Type definition
↓
{
  "type": "card_payin",
  "typeVersion": 1,
  "status": "active",
  "payment": {
    "amount": "{{order_amount}}",
    "direction": "payin"
  },
  "accounting": {
    "settled": {
      "description": "Order {{order_id}} settled",
      "lines": [
        {
          "key": "settlement",
          "system": "payment_settlement_line",
          "account": {
            "path": "assets-root/payments"
          },
          "amount": "{{settled_amount}}"
        },
        {
          "key": "fee",
          "system": "payment_fee_line",
          "account": {
            "path": "assets-root/payments"
          },
          "amount": "-{{fragment_fee_amount}}"
        },
        {
          "key": "receivable",
          "account": {
            "path": "assets-root/receivable:{{order_id}}"
          },
          "amount": "-{{settled_amount}}"
        },
        {
          "key": "fee-expense",
          "account": {
            "path": "expense-root/payment-fees"
          },
          "amount": "{{fragment_fee_amount}}"
        }
      ]
    }
  }
}
 

b. Embed Checkout

#

Add checkout to your product in two steps:

  1. Create a Payment via the API
  2. Pass the response to FRAGMENT's Checkout form
Checkout
  1. Browser
  2. Your server
  3. FRAGMENT
  4. Card rail
    1. createPayment
    2. Payment
    1. Payment
    2. confirm
    1. capture
    2. captured
Create a Payment#

Call createPayment from your backend with a Payment Type:

const { createPayment } = await fragment.createPayment({
  ik: orderId,
  ledgerIk: 'main',
  type: 'card_payin',
  typeVersion: 1,
  parameters: {
    amount: '4999',
  },
});

if (createPayment.__typename !== 'CreatePaymentResult') {
  throw new Error(createPayment.message);
}

return { payment: createPayment.payment };

The Payment starts in needs_payment_method. No money moves until the customer confirms the card.

Render the checkout form#

Use FRAGMENT's frontend SDK to render a checkout form.

Pass the Payment and the customer's email in billingDetails.email as props to the component. Mount Checkout within PaymentElements:

CheckoutPage.tsx
import {
  Checkout,
  PaymentElements,
  type Payment,
} from '@fragment-dev/payment-elements';
import { PayButton } from './PayButton';

const CheckoutPage = ({
  payment,
  email,
  onSuccess,
}: {
  payment: Payment;
  email: string;
  onSuccess: () => void;
}) => (
  <PaymentElements>
    <Checkout
      payment={payment}
      billingDetails={{ email }}
      onError={(message) => console.error('Payment failed:', message)}
      onConfirmed={onSuccess}
    >
      <PayButton />
    </Checkout>
  </PaymentElements>
);

Implement a Pay button. Call confirm() to charge the card when canConfirm is true:

PayButton.tsx
import { usePayment } from '@fragment-dev/payment-elements';

export const PayButton = () => {
  const { confirm, canConfirm, isConfirming, isLoading, error } = usePayment();
  return (
    <>
      <button
        type="button"
        disabled={isLoading || !canConfirm || isConfirming}
        onClick={() => canConfirm && confirm()}
      >
        Pay
      </button>
      {error && <p role="alert">{error}</p>}
    </>
  );
};

The SDK calls onConfirmed after it charges the card. On a failure, it calls onError.

c. Manage saved cards

#

Save a user's cards during checkout or onboarding to initiate payments from your backend.

Save a card at checkout#

Enable savePaymentMethod on the Checkout component to save a user's card when the payment confirms:

CheckoutPage.tsx
const CheckoutPage = ({ payment, email }: { payment: Payment; email: string }) => {
  const [saveCard, setSaveCard] = useState(false);
  return (
    <Checkout
      payment={payment}
      billingDetails={{ email }}
      savePaymentMethod={saveCard}
      onError={logError}
      onConfirmed={showReceipt}
    >
      <SaveCardCheckbox checked={saveCard} onChange={setSaveCard} />
      <PayButton />
    </Checkout>
  );
};

Render a checkbox for the payer to choose during checkout:

Saved cards are tied to the user's email provided in billingDetails.email.

Save a card#

Save a user's card during onboarding to charge them later in a couple of steps:

  1. Create a PaymentMethodSetup via the API
  2. Pass the response to FRAGMENT's SavePaymentMethod component
Save a card
  1. Browser
  2. Your server
  3. FRAGMENT
    1. createPaymentMethodSetup
    2. PaymentMethodSetup
    1. PaymentMethodSetup
    1. submit
    2. submitted

Call createPaymentMethodSetup from your backend:

const { createPaymentMethodSetup } = await fragment.createPaymentMethodSetup({
  ik: requestId,
  ledgerIk: 'main',
  email,
});

if (createPaymentMethodSetup.__typename !== 'CreatePaymentMethodSetupResult') {
  throw new Error(createPaymentMethodSetup.message);
}

return { paymentMethodSetup: createPaymentMethodSetup.paymentMethodSetup };

Pass the returned PaymentMethodSetup to your frontend. Render SavePaymentMethod from FRAGMENT's frontend SDK inside PaymentElements to display the card form:

Implement a Save button. Call submit() from usePaymentMethodSetup() when canSubmit is true:

SaveCardPage.tsx
↓
import {
  PaymentElements,
  SavePaymentMethod,
  usePaymentMethodSetup,
  type PaymentMethodSetup,
} from '@fragment-dev/payment-elements';

const SaveButton = () => {
  const { submit, canSubmit, state, error } = usePaymentMethodSetup();
  return (
    <>
      <button type="button" disabled={!canSubmit} onClick={() => void submit()}>
        {state.status === 'processing' ? 'Saving…' : 'Save card'}
      </button>
      {state.status === 'declined' && <p role="alert">{state.message}</p>}
      {error && <p role="alert">{error.message}</p>}
    </>
  );
};

export const SaveCardPage = ({ setup }: { setup: PaymentMethodSetup }) => (
  <PaymentElements>
    <SavePaymentMethod
      paymentMethodSetup={setup}
      onPaymentMethodSaved={showConfirmation}
      onPaymentMethodDeclined={showDecline}
      onError={logError}
    >
      <SaveButton />
    </SavePaymentMethod>
  </PaymentElements>
);
 

The SDK calls onPaymentMethodSaved after it saves the card, and onPaymentMethodDeclined when the card is declined. Any other failure calls onError.

Charge a saved card#

Call createPayment with the saved card's id in paymentMethod to make a payment from your backend:

const { createPayment } = await fragment.createPayment({
  ik: renewalId,
  ledgerIk: 'main',
  type: 'card_payin',
  typeVersion: 1,
  parameters: {
    amount: '4999',
  },
  paymentMethod: { id: savedCardId },
});

The Payment starts in processing and follows the same lifecycle as a checkout Payment.

Delete a saved card#

Call deletePaymentMethod with the saved card's id. After the call, the card can no longer be charged:

await fragment.deletePaymentMethod({
  id: savedCardId,
});

d. Refund a Payment

#

To refund a settled Payment, call refundPayment with the Payment's ik:

const { payment, refund } = await fragment.refundPayment({
  ik: refundRequestIk,
  paymentId: settledPayment.ik,
});

console.log(refund.status); // pending
console.log(refund.amount); // 4999

Pass amount to refund part of the Payment, up to its captured amount:

const { payment, refund } = await fragment.refundPayment({
  ik: refundRequestId,
  paymentId: settledPayment.id,
  amount: '2500',
});

The funds will immediately move out of your Payment Account and the refund will move to a pending status. Refunds will move to the succeeded status after a few days. In the event of a failure, the refund will be marked as failed.

FRAGMENT keeps its processing fee from the original Payment when you refund it.

e. Testing

#

Use the following cards in sandbox mode for your testing:

Card numberOutcome
4242 4242 4242 4242The payment succeeds. A saved card's later charges also succeed.
4000 0000 0000 0002The payment is declined.
4000 0000 0000 0341The card saves, but a later charge against it is declined.
5385 3083 6013 51813D Secure is required. Enter Checkout1! on the 3DS screen.

The expiry date must be in the future, and use any three-digit CVC.