@rytass/invoice-adapter-bank-pro
v0.2.14
Published
Rytass Invoice Gateway - BankPro
Downloads
753
Readme
Rytass Utils - Invoice Adapter Bank Pro
A comprehensive Bank Pro electronic invoice integration adapter for Taiwan's electronic invoice system. This adapter provides seamless integration with Bank Pro's B2B2C Web API, supporting complete invoice lifecycle management with robust tax compliance and carrier support.
Features
- [x] Complete Bank Pro B2B2C Web API integration
- [x] B2B and B2C invoice issuance
- [x] Invoice query functionality (by order ID or invoice number)
- [x] Invoice voiding operations
- [x] Invoice allowance (partial refund) management
- [x] Allowance invalidation support
- [x] Multiple carrier type support
- [x] Tax calculation and validation
- [x] Detailed product information support
- [x] Email notification integration
- [x] Production and development environment support
- [x] TypeScript type safety
- [x] Comprehensive error handling
Installation
npm install @rytass/invoice-adapter-bank-pro
# or
yarn add @rytass/invoice-adapter-bank-proPeer Dependencies:
npm install @rytass/invoiceConfiguration
Environment Setup
import { BankProInvoiceGateway, BankProBaseUrls } from '@rytass/invoice-adapter-bank-pro';
// Development environment
const developmentGateway = new BankProInvoiceGateway({
user: 'YOUR_DEVELOPMENT_USER',
password: 'YOUR_DEVELOPMENT_PASSWORD',
systemOID: 12345, // Your development system OID
sellerBAN: '12345678', // Your company VAT number
baseUrl: BankProBaseUrls.DEVELOPMENT, // http://webtest.bpscm.com.tw/webapi/api/B2B2CWebApi
});
// Production environment
const productionGateway = new BankProInvoiceGateway({
user: 'YOUR_PRODUCTION_USER',
password: 'YOUR_PRODUCTION_PASSWORD',
systemOID: 54321, // Your production system OID
sellerBAN: '12345678', // Your company VAT number
baseUrl: BankProBaseUrls.PRODUCTION, // http://www.bpscm.com.tw/webapi/api/B2B2CWebApi
});Configuration Options
interface BankProInvoiceGatewayOptions {
user: string; // Bank Pro API user ID
password: string; // Bank Pro API password
systemOID: number; // Bank Pro system identifier
sellerBAN: string; // Seller's VAT number (統編)
baseUrl?: BankProBaseUrls; // API base URL (development/production)
}Basic Usage
B2C Invoice Issuance
import { BankProInvoiceGateway, InvoiceCarriers, TaxType } from '@rytass/invoice-adapter-bank-pro';
const gateway = new BankProInvoiceGateway({
user: process.env.BANKPRO_USER!,
password: process.env.BANKPRO_PASSWORD!,
systemOID: parseInt(process.env.BANKPRO_SYSTEM_OID!),
sellerBAN: process.env.SELLER_VAT_NUMBER!,
baseUrl: BankProBaseUrls.PRODUCTION,
});
// Issue B2C invoice
const invoice = await gateway.issue({
orderId: 'ORD-2024-001',
buyerEmail: '[email protected]',
buyerName: 'Customer Name',
buyerAddress: '台北市信義區信義路五段7號',
buyerZipCode: '110',
buyerMobile: '0912345678',
carrier: InvoiceCarriers.PRINT,
items: [
{
name: 'Premium Product',
unitPrice: 2000,
quantity: 1,
unit: '個',
taxType: TaxType.TAXED,
id: 'PROD-001',
barcode: '1234567890123',
spec: '高級款',
remark: '限量商品',
},
{
name: 'Standard Service',
unitPrice: 1500,
quantity: 2,
unit: '次',
taxType: TaxType.TAXED,
id: 'SRV-001',
},
],
});
console.log('Invoice Number:', invoice.number);
console.log('Random Code:', invoice.randomCode);
console.log('Total Amount:', invoice.amount);B2B Invoice Issuance
// B2B invoice with company information
const b2bInvoice = await gateway.issue({
orderId: 'B2B-2024-001',
vatNumber: '12345678', // Buyer's VAT number
companyName: '採購公司股份有限公司',
buyerEmail: '[email protected]',
buyerName: '採購部經理',
buyerAddress: '新北市板橋區文化路二段242號',
buyerZipCode: '220',
carrier: InvoiceCarriers.PRINT,
sellerCode: 'SELLER-001', // Optional seller code
remark: 'B2B訂單 - 月結30天',
items: [
{
name: '企業方案服務',
unitPrice: 50000,
quantity: 1,
unit: '年',
taxType: TaxType.TAXED,
id: 'ENT-PLAN-001',
spec: '專業版',
remark: '包含技術支援',
},
{
name: '客製化開發',
unitPrice: 100000,
quantity: 1,
unit: '式',
taxType: TaxType.TAXED,
id: 'CUSTOM-DEV-001',
},
],
});Invoice with Mobile Carrier
// Issue invoice with mobile barcode carrier
const mobileInvoice = await gateway.issue({
orderId: 'MOBILE-2024-001',
buyerEmail: '[email protected]',
buyerName: 'Mobile User',
carrier: InvoiceCarriers.MOBILE('/ABC12345'),
items: [
{
name: 'Digital Product',
unitPrice: 1200,
quantity: 1,
unit: '個',
taxType: TaxType.TAXED,
},
],
});Invoice with Love Code (Donation)
// Issue invoice with donation to charity
const donationInvoice = await gateway.issue({
orderId: 'DONATION-2024-001',
buyerEmail: '[email protected]',
buyerName: 'Charitable Donor',
carrier: InvoiceCarriers.LOVE_CODE('001'),
items: [
{
name: 'Charity Purchase',
unitPrice: 1000,
quantity: 3,
unit: '份',
taxType: TaxType.TAXED,
},
],
});Advanced Usage
Mixed Tax Type Items
const mixedTaxInvoice = await gateway.issue({
orderId: 'MIXED-2024-001',
buyerEmail: '[email protected]',
buyerName: 'Export Customer',
buyerAddress: 'Export Address',
customsMark: CustomsMark.YES,
carrier: InvoiceCarriers.PRINT,
items: [
{
name: 'Domestic Product',
unitPrice: 1000,
quantity: 2,
taxType: TaxType.TAXED, // 5% tax
unit: '個',
},
{
name: 'Tax-Free Product',
unitPrice: 2000,
quantity: 1,
taxType: TaxType.TAX_FREE, // No tax
unit: '件',
},
{
name: 'Export Product',
unitPrice: 3000,
quantity: 1,
taxType: TaxType.ZERO_TAX, // Export (0% tax)
unit: '組',
},
],
});Detailed Product Information
const detailedInvoice = await gateway.issue({
orderId: 'DETAILED-2024-001',
buyerEmail: '[email protected]',
buyerName: 'Detail Customer',
carrier: InvoiceCarriers.PRINT,
items: [
{
name: 'Smart Phone',
unitPrice: 25000,
quantity: 1,
unit: '支',
taxType: TaxType.TAXED,
id: 'PHONE-001',
barcode: '4710543211234',
spec: '128GB 藍色',
remark: '保固一年',
},
{
name: 'Phone Case',
unitPrice: 500,
quantity: 2,
unit: '個',
taxType: TaxType.TAXED,
id: 'CASE-001',
barcode: '4710543211241',
spec: '透明矽膠',
remark: '防摔保護',
},
],
});Query Operations
Query by Order ID
// Query invoice using order ID
const queriedByOrder = await gateway.query({
orderId: 'ORD-2024-001',
});
console.log('Invoice Number:', queriedByOrder.number);
console.log('Invoice State:', queriedByOrder.state);
console.log('Invoice Amount:', queriedByOrder.amount);
console.log('Tax Amount:', queriedByOrder.taxAmount);Query by Invoice Number
// Query invoice using invoice number
const queriedByNumber = await gateway.query({
invoiceNumber: 'AA12345678',
});
console.log('Order ID:', queriedByNumber.orderId);
console.log('Random Code:', queriedByNumber.randomCode);
console.log('Issued On:', queriedByNumber.issuedOn);
console.log('Void Status:', queriedByNumber.state);Invoice Management
Invoice Voiding
// Query and void an invoice
const invoiceToVoid = await gateway.query({
orderId: 'ORD-2024-001',
});
const voidedInvoice = await gateway.void(invoiceToVoid, {
reason: 'Customer requested cancellation due to shipping delay',
});
console.log('Void Status:', voidedInvoice.state); // InvoiceState.VOID
console.log('Voided On:', voidedInvoice.voidOn);Invoice Allowance (Partial Refund)
// Create allowance for partial refund
const originalInvoice = await gateway.query({
orderId: 'ORD-2024-001',
});
const allowanceInvoice = await gateway.allowance(originalInvoice, [
{
name: 'Returned Product',
unitPrice: 2000,
quantity: 1, // Return 1 item
unit: '個',
taxType: TaxType.TAXED,
id: 'RETURN-001',
},
]);
console.log('Allowance Number:', allowanceInvoice.allowances[0].number);
console.log('Allowance Amount:', allowanceInvoice.allowances[0].amount);
console.log('Remaining Amount:', allowanceInvoice.allowances[0].remainingAmount);Invalidate Allowance
// Invalidate a previously issued allowance
const allowanceToInvalidate = allowanceInvoice.allowances[0];
const updatedInvoice = await gateway.invalidAllowance(allowanceToInvalidate);
console.log('Allowance invalidated:', updatedInvoice.allowances[0].invalidatedOn);Error Handling
Bank Pro Specific Errors
try {
const invoice = await gateway.issue({
orderId: 'ERROR-TEST',
buyerEmail: '[email protected]',
carrier: InvoiceCarriers.PRINT,
items: [
/* invalid items */
],
});
} catch (error) {
if (error.message.includes('authentication failed')) {
console.error('Bank Pro authentication failed - check credentials');
} else if (error.message.includes('system unavailable')) {
console.error('Bank Pro system temporarily unavailable');
} else if (error.message.includes('duplicate order')) {
console.error('Order ID already exists - use different order ID');
} else if (error.message.includes('invalid VAT number')) {
console.error('Buyer VAT number validation failed');
} else {
console.error('Invoice issuance failed:', error.message);
}
}Network and System Errors
async function safeInvoiceOperation<T>(operation: () => Promise<T>, retries: number = 3): Promise<T> {
for (let attempt = 1; attempt <= retries; attempt++) {
try {
return await operation();
} catch (error) {
if (attempt === retries) {
throw error;
}
if (error.code === 'ECONNRESET' || error.code === 'ENOTFOUND') {
console.warn(`Network error on attempt ${attempt}, retrying...`);
await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
} else {
throw error; // Don't retry non-network errors
}
}
}
throw new Error('All retry attempts failed');
}
// Usage with retry logic
const invoice = await safeInvoiceOperation(() =>
gateway.issue({
orderId: 'RETRY-001',
buyerEmail: '[email protected]',
carrier: InvoiceCarriers.PRINT,
items: [
/* items */
],
}),
);Integration Examples
Express.js API Integration
import express from 'express';
import { BankProInvoiceGateway, BankProBaseUrls, InvoiceCarriers, TaxType } from '@rytass/invoice-adapter-bank-pro';
const app = express();
const gateway = new BankProInvoiceGateway({
user: process.env.BANKPRO_USER!,
password: process.env.BANKPRO_PASSWORD!,
systemOID: parseInt(process.env.BANKPRO_SYSTEM_OID!),
sellerBAN: process.env.SELLER_VAT_NUMBER!,
baseUrl: process.env.NODE_ENV === 'production' ? BankProBaseUrls.PRODUCTION : BankProBaseUrls.DEVELOPMENT,
});
// Issue invoice endpoint
app.post('/api/invoices', async (req, res) => {
try {
const { orderId, customer, items, isB2B } = req.body;
const invoiceData = {
orderId,
buyerEmail: customer.email,
buyerName: customer.name,
buyerAddress: customer.address,
buyerZipCode: customer.zipCode,
buyerMobile: customer.mobile,
carrier: customer.mobileBarcode ? InvoiceCarriers.MOBILE(customer.mobileBarcode) : InvoiceCarriers.PRINT,
items: items.map(item => ({
name: item.name,
unitPrice: item.price,
quantity: item.quantity,
unit: item.unit || '個',
taxType: TaxType.TAXED,
id: item.productId,
barcode: item.barcode,
spec: item.specification,
remark: item.notes,
})),
};
// Add B2B specific fields
if (isB2B && customer.vatNumber) {
Object.assign(invoiceData, {
vatNumber: customer.vatNumber,
companyName: customer.companyName,
});
}
const invoice = await gateway.issue(invoiceData as any);
res.json({
success: true,
invoice: {
number: invoice.number,
randomCode: invoice.randomCode,
amount: invoice.amount,
taxAmount: invoice.taxAmount,
issuedOn: invoice.issuedOn,
},
});
} catch (error) {
res.status(400).json({
success: false,
error: error.message,
});
}
});
// Query invoice endpoint
app.get('/api/invoices/order/:orderId', async (req, res) => {
try {
const { orderId } = req.params;
const invoice = await gateway.query({ orderId });
res.json({
success: true,
invoice: {
number: invoice.number,
orderId: invoice.orderId,
state: invoice.state,
amount: invoice.amount,
issuedOn: invoice.issuedOn,
},
});
} catch (error) {
res.status(404).json({
success: false,
error: 'Invoice not found',
});
}
});
// Void invoice endpoint
app.post('/api/invoices/:orderId/void', async (req, res) => {
try {
const { orderId } = req.params;
const { reason } = req.body;
const invoice = await gateway.query({ orderId });
const voidedInvoice = await gateway.void(invoice, { reason });
res.json({
success: true,
invoice: {
number: voidedInvoice.number,
state: voidedInvoice.state,
voidedOn: voidedInvoice.voidOn,
},
});
} catch (error) {
res.status(400).json({
success: false,
error: error.message,
});
}
});NestJS Service Integration
import { Injectable, BadRequestException, NotFoundException } from '@nestjs/common';
import { BankProInvoiceGateway, BankProBaseUrls, InvoiceCarriers, TaxType } from '@rytass/invoice-adapter-bank-pro';
@Injectable()
export class BankProInvoiceService {
private readonly gateway: BankProInvoiceGateway;
constructor() {
this.gateway = new BankProInvoiceGateway({
user: process.env.BANKPRO_USER!,
password: process.env.BANKPRO_PASSWORD!,
systemOID: parseInt(process.env.BANKPRO_SYSTEM_OID!),
sellerBAN: process.env.SELLER_VAT_NUMBER!,
baseUrl: process.env.NODE_ENV === 'production' ? BankProBaseUrls.PRODUCTION : BankProBaseUrls.DEVELOPMENT,
});
}
async createInvoiceForOrder(order: {
id: string;
customer: {
email: string;
name: string;
address?: string;
zipCode?: string;
mobile?: string;
vatNumber?: string;
companyName?: string;
mobileBarcode?: string;
};
items: Array<{
name: string;
price: number;
quantity: number;
unit?: string;
productId?: string;
barcode?: string;
specification?: string;
notes?: string;
}>;
notes?: string;
}) {
// Determine invoice type
const isB2B = !!order.customer.vatNumber;
// Determine carrier
let carrier = InvoiceCarriers.PRINT;
if (!isB2B && order.customer.mobileBarcode) {
// Validate mobile barcode if needed
const isValid = await this.gateway.isMobileBarcodeValid(order.customer.mobileBarcode);
if (isValid) {
carrier = InvoiceCarriers.MOBILE(order.customer.mobileBarcode);
}
}
// Prepare invoice data
const invoiceData = {
orderId: order.id,
buyerEmail: order.customer.email,
buyerName: order.customer.name,
buyerAddress: order.customer.address,
buyerZipCode: order.customer.zipCode,
buyerMobile: order.customer.mobile,
carrier,
remark: order.notes,
items: order.items.map(item => ({
name: item.name,
unitPrice: item.price,
quantity: item.quantity,
unit: item.unit || '個',
taxType: TaxType.TAXED,
id: item.productId,
barcode: item.barcode,
spec: item.specification,
remark: item.notes,
})),
};
// Add B2B specific fields
if (isB2B) {
Object.assign(invoiceData, {
vatNumber: order.customer.vatNumber,
companyName: order.customer.companyName,
});
}
const invoice = await this.gateway.issue(invoiceData as any);
return {
invoiceNumber: invoice.number,
randomCode: invoice.randomCode,
amount: invoice.amount,
taxAmount: invoice.taxAmount,
issuedOn: invoice.issuedOn,
};
}
async queryInvoiceByOrderId(orderId: string) {
try {
const invoice = await this.gateway.query({ orderId });
return {
invoiceNumber: invoice.number,
orderId: invoice.orderId,
randomCode: invoice.randomCode,
state: invoice.state,
amount: invoice.amount,
taxAmount: invoice.taxAmount,
issuedOn: invoice.issuedOn,
voidOn: invoice.voidOn,
};
} catch (error) {
throw new NotFoundException('Invoice not found');
}
}
async voidInvoice(orderId: string, reason: string) {
const invoice = await this.gateway.query({ orderId });
if (invoice.state === InvoiceState.VOID) {
throw new BadRequestException('Invoice is already voided');
}
const voidedInvoice = await this.gateway.void(invoice, { reason });
return {
invoiceNumber: voidedInvoice.number,
state: voidedInvoice.state,
voidedOn: voidedInvoice.voidOn,
};
}
async createAllowance(
orderId: string,
allowanceItems: Array<{
name: string;
price: number;
quantity: number;
unit?: string;
productId?: string;
}>,
) {
const originalInvoice = await this.gateway.query({ orderId });
const allowanceInvoice = await this.gateway.allowance(
originalInvoice,
allowanceItems.map(item => ({
name: item.name,
unitPrice: item.price,
quantity: item.quantity,
unit: item.unit || '個',
taxType: TaxType.TAXED,
id: item.productId,
})),
);
return {
allowanceNumber: allowanceInvoice.allowances[0].number,
allowanceAmount: allowanceInvoice.allowances[0].amount,
remainingAmount: allowanceInvoice.allowances[0].remainingAmount,
allowancedOn: allowanceInvoice.allowances[0].issuedOn,
};
}
}E-commerce Platform Integration
import { BankProInvoiceGateway, InvoiceCarriers, TaxType, CustomsMark } from '@rytass/invoice-adapter-bank-pro';
class EcommerceBankProInvoiceProcessor {
constructor(private gateway: BankProInvoiceGateway) {}
async processOrderInvoice(order: {
id: string;
type: 'b2c' | 'b2b';
customer: {
email: string;
name: string;
address?: string;
zipCode?: string;
mobile?: string;
vatNumber?: string;
companyName?: string;
preferences: {
carrier?: 'print' | 'mobile' | 'love_code';
carrierCode?: string;
};
};
items: Array<{
name: string;
price: number;
quantity: number;
unit?: string;
category: string;
productId: string;
barcode?: string;
specification?: string;
notes?: string;
isExport?: boolean;
}>;
shipping: {
isExport?: boolean;
notes?: string;
};
seller?: {
code?: string; // Seller code for multi-vendor platforms
};
}) {
// Determine carrier based on customer preferences
let carrier = InvoiceCarriers.PRINT;
if (order.type === 'b2c') {
switch (order.customer.preferences.carrier) {
case 'mobile':
if (order.customer.preferences.carrierCode) {
// Validate mobile barcode if needed
const isValidBarcode = await this.gateway.isMobileBarcodeValid(order.customer.preferences.carrierCode);
if (isValidBarcode) {
carrier = InvoiceCarriers.MOBILE(order.customer.preferences.carrierCode);
}
}
break;
case 'love_code':
if (order.customer.preferences.carrierCode) {
const isValidLoveCode = await this.gateway.isLoveCodeValid(order.customer.preferences.carrierCode);
if (isValidLoveCode) {
carrier = InvoiceCarriers.LOVE_CODE(order.customer.preferences.carrierCode);
}
}
break;
}
}
// Map items with appropriate tax types
const invoiceItems = order.items.map((item, index) => ({
name: item.name,
unitPrice: item.price,
quantity: item.quantity,
unit: item.unit || '個',
taxType: item.isExport ? TaxType.ZERO_TAX : TaxType.TAXED,
id: item.productId,
barcode: item.barcode,
spec: item.specification,
remark: item.notes,
}));
// Prepare invoice options
const invoiceOptions = {
orderId: order.id,
buyerEmail: order.customer.email,
buyerName: order.customer.name,
buyerAddress: order.customer.address,
buyerZipCode: order.customer.zipCode,
buyerMobile: order.customer.mobile,
carrier,
sellerCode: order.seller?.code,
remark: order.shipping.notes,
items: invoiceItems,
customsMark: order.shipping.isExport ? CustomsMark.YES : CustomsMark.NO,
};
// Add B2B specific fields
if (order.type === 'b2b') {
Object.assign(invoiceOptions, {
vatNumber: order.customer.vatNumber,
companyName: order.customer.companyName,
});
}
const invoice = await this.gateway.issue(invoiceOptions as any);
// Store invoice reference
await this.storeInvoiceReference(order.id, invoice.number, invoice.randomCode);
return {
invoiceNumber: invoice.number,
randomCode: invoice.randomCode,
amount: invoice.amount,
taxAmount: invoice.taxAmount,
issuedOn: invoice.issuedOn,
};
}
async processOrderRefund(
orderId: string,
refundItems: Array<{
productId: string;
name: string;
price: number;
quantity: number;
unit?: string;
}>,
) {
// Query original invoice
const originalInvoice = await this.gateway.query({ orderId });
// Create allowance for refunded items
const allowanceInvoice = await this.gateway.allowance(
originalInvoice,
refundItems.map(item => ({
name: item.name,
unitPrice: item.price,
quantity: item.quantity,
unit: item.unit || '個',
taxType: TaxType.TAXED,
id: item.productId,
})),
);
return {
allowanceNumber: allowanceInvoice.allowances[0].number,
allowanceAmount: allowanceInvoice.allowances[0].amount,
remainingAmount: allowanceInvoice.allowances[0].remainingAmount,
};
}
private async storeInvoiceReference(orderId: string, invoiceNumber: string, randomCode: string) {
// Implementation to store invoice-order relationship in database
}
}Best Practices
Configuration Management
- Store sensitive credentials in environment variables
- Use different configurations for development and production environments
- Regularly rotate API credentials and passwords
- Implement proper error handling for authentication failures
Invoice Processing
- Always validate carrier information before invoice issuance
- Implement proper retry logic for network failures
- Store order IDs and invoice mappings for future reference
- Handle duplicate order IDs appropriately
Tax Compliance
- Use correct tax types for different product categories
- Handle export/import scenarios with appropriate customs marks
- Validate VAT numbers for B2B transactions
- Maintain detailed product information for audit purposes
Performance Optimization
- Implement connection pooling for high-volume processing
- Use appropriate timeout settings for API calls
- Cache frequently accessed invoice data
- Monitor API response times and implement alerting
Security
- Validate all input parameters before processing
- Sanitize customer data and product information
- Log all invoice operations for audit trails
- Implement proper access controls for invoice management functions
Testing
import { BankProInvoiceGateway, BankProBaseUrls, InvoiceCarriers, TaxType } from '@rytass/invoice-adapter-bank-pro';
describe('Bank Pro Invoice Integration', () => {
let gateway: BankProInvoiceGateway;
beforeEach(() => {
gateway = new BankProInvoiceGateway({
user: 'test_user',
password: 'test_password',
systemOID: 12345,
sellerBAN: '12345678',
baseUrl: BankProBaseUrls.DEVELOPMENT,
});
});
it('should issue B2C invoice successfully', async () => {
const invoice = await gateway.issue({
orderId: 'TEST-001',
buyerEmail: '[email protected]',
buyerName: 'Test Customer',
carrier: InvoiceCarriers.PRINT,
items: [
{
name: 'Test Product',
unitPrice: 1000,
quantity: 1,
unit: '個',
taxType: TaxType.TAXED,
},
],
});
expect(invoice.number).toBeDefined();
expect(invoice.amount).toBe(1050); // Including 5% tax
expect(invoice.state).toBe(InvoiceState.ISSUED);
});
it('should query invoice by order ID', async () => {
const queriedInvoice = await gateway.query({
orderId: 'TEST-001',
});
expect(queriedInvoice.orderId).toBe('TEST-001');
expect(queriedInvoice.number).toBeDefined();
});
it('should create allowance successfully', async () => {
const originalInvoice = await gateway.query({
orderId: 'TEST-001',
});
const allowanceInvoice = await gateway.allowance(originalInvoice, [
{
name: 'Returned Product',
unitPrice: 1000,
quantity: 1,
unit: '個',
taxType: TaxType.TAXED,
},
]);
expect(allowanceInvoice.allowances).toHaveLength(1);
expect(allowanceInvoice.allowances[0].amount).toBe(1050);
});
});License
MIT
