Beta sürümün dokümanını görüntülüyorsunuz.Stabil dokümana geç
Better Payment
Rehberler

Özel Sağlayıcılar

Better Payment'in henüz desteklemediği bir ödeme sağlayıcısını, yerleşik olanlarla aynı API ile ekleyin.

Beta sürümün dokümanını görüntülüyorsunuz

Beta sürümü kurmak için npm install better-payment@beta. Stabil dokümana geç

Sağlayıcı, PaymentProvider sınıfını genişleten bir sınıftır. Yerleşik sağlayıcılar da aynı şekilde yazılır; bu yüzden özel bir sağlayıcı diğer her şeyle çalışır: plugin'ler ve olaylar, HTTP handler (/api/pay/<id>/... altında), tarayıcı client'ı (client.use('<id>')) ve istek doğrulama.

Sağlayıcı başkalarının da işine yarayacaksa (bir Türk bankası ya da ödeme kuruluşu), onu Better Payment'e eklemeyi düşünün. Depodaki katkı rehberine bakın.

Sağlayıcıyı yazmak

Altı soyut metodu yazın: createPayment, initThreeDSPayment, completeThreeDSPayment, refund, cancel ve getPayment. Diğerleri (authorize, capture, saveCard, binCheck, installmentInfo, …) siz yazana kadar NOT_SUPPORTED fırlatır.

import {
  betterPayment,
  defineProvider,
  hmac,
  toHex,
  PaymentProvider,
  PaymentStatus,
  PaymentErrorCode,
  ConfigurationError,
  ValidationError,
  HttpError,
  type PaymentProviderConfig,
  type PaymentRequest,
  type PaymentResponse,
  type ThreeDSPaymentRequest,
  type ThreeDSInitResponse,
  type RefundRequest,
  type RefundResponse,
  type CancelRequest,
  type CancelResponse,
} from 'better-payment';

export interface MyPosConfig extends PaymentProviderConfig {
  terminalId: string;
  secretKey: string;
}

interface MyPosResult {
  approved: boolean;
  transactionId?: string;
  responseCode?: string;
  message?: string;
}

export class MyPos extends PaymentProvider<MyPosConfig> {
  private readonly http = this.createHttpClient('MyPos', { timeout: 30_000 });

  // Called by the base constructor: throw on missing credentials
  protected validateConfig(): void {
    if (!this.config.terminalId || !this.config.secretKey) {
      throw new ConfigurationError('MyPos: terminalId and secretKey are required', 'mypos');
    }
  }

  // The provider's error codes, mapped to the normalized ones
  protected errorCodeTable(): Record<string, PaymentErrorCode> {
    return { '51': PaymentErrorCode.INSUFFICIENT_FUNDS, '54': PaymentErrorCode.EXPIRED_CARD };
  }

  async createPayment(request: PaymentRequest): Promise<PaymentResponse> {
    try {
      // Card, amounts and required fields: throws a ValidationError before any API call
      this.validatePayment(request, { card: true, required: ['buyer.ip'] });
      const body = JSON.stringify({
        terminalId: this.config.terminalId,
        orderId: request.conversationId,
        amount: request.paidPrice,
        cardNumber: this.cardOf(request).cardNumber,
      });
      const { data } = await this.http.post<MyPosResult>('/payments', body, {
        headers: { 'x-signature': toHex(await hmac('SHA-256', this.config.secretKey, body)) },
      });
      return this.withErrorCode({
        status: data.approved ? PaymentStatus.SUCCESS : PaymentStatus.FAILURE,
        paymentId: data.transactionId,
        conversationId: request.conversationId,
        errorCode: data.responseCode,
        errorMessage: data.message,
        rawResponse: data,
      });
    } catch (error) {
      if (error instanceof ValidationError) {
        return this.withErrorCode({
          status: PaymentStatus.FAILURE,
          errorCode: 'VALIDATION_ERROR',
          errorMessage: error.message,
        });
      }
      if (error instanceof HttpError && error.isNetworkError) {
        // No response: the payment may have gone through. Never report it as failed.
        return this.withErrorCode({
          status: PaymentStatus.PENDING,
          errorCode: 'NETWORK_ERROR',
          conversationId: request.conversationId,
        });
      }
      throw error;
    }
  }

  async initThreeDSPayment(_request: ThreeDSPaymentRequest): Promise<ThreeDSInitResponse> {
    throw this.notSupported('3D Secure');
  }

  async completeThreeDSPayment(_callbackData: unknown): Promise<PaymentResponse> {
    throw this.notSupported('3D Secure');
  }

  async refund(request: RefundRequest): Promise<RefundResponse> {
    this.validateRefund(request);
    const { data } = await this.http.post<MyPosResult>(
      '/refunds',
      JSON.stringify({ transactionId: request.paymentId, amount: request.price })
    );
    return this.withErrorCode({
      status: data.approved ? PaymentStatus.SUCCESS : PaymentStatus.FAILURE,
      refundId: data.transactionId,
      errorCode: data.responseCode,
    });
  }

  async cancel(_request: CancelRequest): Promise<CancelResponse> {
    throw this.notSupported('Cancel');
  }

  async getPayment(paymentId: string): Promise<PaymentResponse> {
    const { data } = await this.http.get<MyPosResult>(`/payments/${encodeURIComponent(paymentId)}`);
    return {
      status: data.approved ? PaymentStatus.SUCCESS : PaymentStatus.FAILURE,
      paymentId,
      rawResponse: data,
    };
  }
}

Temel sınıfın sağladıkları:

ÜyeKullanımı
this.configConfig; varsayılan olarak locale: 'tr' ile
validateConfig()Kimlik bilgilerini kontrol etmek için yazın; constructor onu çağırır
createHttpClient(name, { timeout })Yapılandırılmış baseUrl, logger, fetch ve yeniden deneme politikasıyla fetch tabanlı bir client. Yalnızca idempotent istekler yeniden denenir
validatePayment(request, rules), validateRefund(request)İstek doğrulama. Geçersiz bir istek ValidationError fırlatır: bunu başarısız bir sonuç olarak döndürün; withErrorCode ona INVALID_REQUEST kodunu verir
errorCodeTable() ve withErrorCode(result)Sağlayıcının hata kodlarını normalize kodlara eşler; withErrorCode başarısız sonuçlara code ekler
cardOf(request)İsteğin kartı ya da bir ValidationError
notSupported(feature)NOT_SUPPORTED hatası; handler buna 400 ile yanıt verir

better-payment'daki hmac, digest, safeEqual, toHex ve toBase64, istekleri WebCrypto ile imzalar ve callback'leri doğrular; böylece sağlayıcı edge ortamlarında da çalışır. İmzaları === ile değil, her zaman safeEqual ile karşılaştırın.

Bir ödemeyi yalnızca sağlayıcı reddettiğinde failure olarak bildirin. Yanıt yoksa errorCode: 'NETWORK_ERROR' ile pending döndürün: ödeme gerçekleşmiş olabilir. Ödeme, iade ve iptal isteklerini asla otomatik olarak yeniden denemeyin. completeThreeDSPayment içinde callback'e güvenmeden önce imzasını doğrulayın ve sahte bir callback için errorCode: 'INVALID_HASH' döndürün.

Kaydetmek

Bir örnek ya da daha iyisi defineProvider() ile yapılmış bir tanım verin. Tanım, yerleşik sağlayıcılarda olduğu gibi, ödeme nesnesi oluşturulurken ortak ayarları (mode, logger, retry, fetch, validate) alır:

/** `providers: { mypos: myPos({ ... }) }`: gets the mode, logger, retry and fetch settings */
export const myPos = (config: MyPosConfig) =>
  defineProvider(
    (ctx) =>
      new MyPos({
        ...config,
        baseUrl:
          config.baseUrl ??
          (ctx.mode === 'sandbox' ? 'https://test.mypos.example' : 'https://api.mypos.example'),
        logger: config.logger ?? ctx.logger,
        retry: config.retry ?? ctx.retry,
        fetch: config.fetch ?? ctx.fetch,
        validate: config.validate ?? ctx.validate,
      })
  );

export const payment = betterPayment({
  providers: {
    mypos: myPos({ terminalId: 'T1', secretKey: 'secret' }),
  },
  mode: 'sandbox',
});

// payment.mypos is a MyPos; routes are /api/pay/mypos/...

providers içindeki anahtar sağlayıcının kimliğidir. payment.mypos, eklediğiniz üyeler dahil MyPos tipindedir.

Test etmek

Sağlayıcıyı API'si olmadan test etmek için fetch'i taklit edin:

import { it, expect, vi } from 'vitest';
import { betterPayment } from 'better-payment';
import { myPos } from './my-pos';

it('charges through MyPos', async () => {
  const fetch = vi.fn(async () => Response.json({ approved: true, transactionId: 'T-1' }));
  const payment = betterPayment({
    providers: { mypos: myPos({ terminalId: 'T1', secretKey: 'secret', fetch }) },
  });

  const result = await payment.createPayment(order);

  expect(result).toMatchObject({ status: 'success', paymentId: 'T-1' });
  expect(fetch).toHaveBeenCalledWith('https://api.mypos.example/payments', expect.anything());
});

On this page