/**
* SmilePay 電子發票 API 客戶端
*
* 透過現有的 smilepay-einvoice MCP server 代理呼叫
* 這樣可以重用已有的 API 實作,不需要重複實作加密邏輯
*/
import { getAppSecrets } from '../../utils/secrets.js'
import { logger } from '../../middleware/logging.js'
import type {
IssueInvoiceParams,
IssueAllowanceParams,
CancelInvoiceParams,
VoidInvoiceParams,
CancelAllowanceParams,
GetPrintUrlParams,
QueryInvoiceParams,
QueryAllowanceParams,
ListInvoicesParams,
} from './types.js'
/** SmilePay API 基礎 URL */
const SMILEPAY_API_URL = 'https://ssl.smse.com.tw/api/SPInvoice.asp'
/**
* SmilePay 電子發票客戶端
*
* 注意:這個客戶端目前透過直接 HTTP 呼叫 SmilePay API
* 如果需要更複雜的加密邏輯,可以改用 MCP server 代理
*/
export class EInvoiceClient {
private merchantNo: string
// hashKey and hashIv are stored for future encryption implementation
// Currently using SmilePay's session-based authentication
public readonly hashKey: string
public readonly hashIv: string
constructor(merchantNo: string, hashKey: string, hashIv: string) {
this.merchantNo = merchantNo
this.hashKey = hashKey
this.hashIv = hashIv
}
/**
* 開立發票
*/
async issueInvoice(params: IssueInvoiceParams): Promise<any> {
logger.info({ invoiceDate: params.invoiceDate, items: params.items.length }, '開立發票')
// 建構 API 請求
const requestData = {
Ession_Key: this.merchantNo,
Invoice_Date: params.invoiceDate,
Invoice_Time: params.invoiceTime || this.getCurrentTime(),
Tax_Type: params.taxType,
All_Amount: params.allAmount,
Buyer_Id: params.buyerId || '',
Company_Name: params.companyName || '',
Buyer_Name: params.name || '',
Buyer_Email: params.email || '',
Buyer_Phone: params.phone || '',
Buyer_Address: params.address || '',
Carrier_Type: params.carrierType || '',
Carrier_Id: params.carrierId || '',
Donate_Mark: params.donateMark || '0',
Love_Key: params.loveKey || '',
Order_Id: params.orderId || '',
Data_Id: params.dataId || '',
Main_Remark: params.mainRemark || '',
Product_Array: params.items.map((item) => ({
Product_Name: item.description,
Product_Qty: item.quantity,
Product_Unit: item.unit || '式',
Product_Price: item.unitPrice,
Product_Amount: item.amount,
Product_Remark: item.remark || '',
})),
}
return await this.callApi('Issue', requestData)
}
/**
* 開立折讓單
*/
async issueAllowance(params: IssueAllowanceParams): Promise<any> {
logger.info({ invoiceNumber: params.invoiceNumber }, '開立折讓單')
const requestData = {
Session_Key: this.merchantNo,
Invoice_No: params.invoiceNumber,
Invoice_Date: params.invoiceDate,
Allowance_Date: params.allowanceDate || this.getCurrentDate(),
Allowance_No: params.allowanceNumber || '',
Allowance_Type: params.allowanceType || '1',
Product_Array: params.items.map(item => ({
Product_Name: item.description,
Product_Qty: item.quantity,
Product_Unit: item.unit || '式',
Product_Price: item.unitPrice,
Product_Amount: item.amount,
Product_Tax: item.tax,
Tax_Type: item.taxType,
})),
}
return await this.callApi('Allowance', requestData)
}
/**
* 作廢發票(未上傳財政部)
*/
async cancelInvoice(params: CancelInvoiceParams): Promise<any> {
logger.info({ invoiceNumber: params.invoiceNumber }, '作廢發票')
const requestData = {
Session_Key: this.merchantNo,
Invoice_No: params.invoiceNumber,
Invoice_Date: params.invoiceDate,
Cancel_Reason: params.cancelReason,
Remark: params.remark || '',
}
return await this.callApi('Cancel', requestData)
}
/**
* 註銷發票(已上傳財政部)
*/
async voidInvoice(params: VoidInvoiceParams): Promise<any> {
logger.info({ invoiceNumber: params.invoiceNumber }, '註銷發票')
const requestData = {
Session_Key: this.merchantNo,
Invoice_No: params.invoiceNumber,
Invoice_Date: params.invoiceDate,
Void_Reason: params.voidReason,
Remark: params.remark || '',
}
return await this.callApi('Void', requestData)
}
/**
* 作廢折讓單
*/
async cancelAllowance(params: CancelAllowanceParams): Promise<any> {
logger.info({ invoiceNumber: params.invoiceNumber, allowanceNumber: params.allowanceNumber }, '作廢折讓單')
const requestData = {
Session_Key: this.merchantNo,
Invoice_No: params.invoiceNumber,
Invoice_Date: params.invoiceDate,
Allowance_No: params.allowanceNumber,
Allowance_Date: params.allowanceDate,
Cancel_Reason: params.cancelReason,
Remark: params.remark || '',
}
return await this.callApi('AllowanceCancel', requestData)
}
/**
* 取得發票列印 URL
*/
async getPrintUrl(params: GetPrintUrlParams): Promise<{ url: string }> {
logger.info({ invoiceNumber: params.invoiceNumber }, '取得列印 URL')
const mode = params.mode || 'web'
const baseUrl = mode === 'web'
? 'https://ssl.smse.com.tw/web/InvoicePrint.asp'
: 'https://ssl.smse.com.tw/epson/InvoicePrint.asp'
const queryParams = new URLSearchParams({
Session_Key: this.merchantNo,
Invoice_No: params.invoiceNumber,
Invoice_Date: params.invoiceDate,
Random_Number: params.randomNumber,
Auto_Print: params.autoPrint ? '1' : '0',
Detail_Print: params.detailPrint ? '1' : '0',
})
return {
url: `${baseUrl}?${queryParams.toString()}`,
}
}
/**
* 查詢發票
*/
async queryInvoice(params: QueryInvoiceParams): Promise<any> {
logger.info({ invoiceNumber: params.invoiceNumber, dataId: params.dataId }, '查詢發票')
const requestData: Record<string, string> = {
Session_Key: this.merchantNo,
}
if (params.invoiceNumber) requestData.Invoice_No = params.invoiceNumber
if (params.invoiceDate) requestData.Invoice_Date = params.invoiceDate
if (params.randomNumber) requestData.Random_Number = params.randomNumber
if (params.dataId) requestData.Data_Id = params.dataId
return await this.callApi('Query', requestData)
}
/**
* 查詢折讓單
*/
async queryAllowance(params: QueryAllowanceParams): Promise<any> {
logger.info({ invoiceNumber: params.invoiceNumber, allowanceNumber: params.allowanceNumber }, '查詢折讓單')
const requestData: Record<string, string> = {
Session_Key: this.merchantNo,
}
if (params.invoiceNumber) requestData.Invoice_No = params.invoiceNumber
if (params.invoiceDate) requestData.Invoice_Date = params.invoiceDate
if (params.allowanceNumber) requestData.Allowance_No = params.allowanceNumber
return await this.callApi('AllowanceQuery', requestData)
}
/**
* 列表發票
*/
async listInvoices(params: ListInvoicesParams): Promise<any> {
logger.info({ startDate: params.startDate, endDate: params.endDate }, '列表發票')
const requestData: Record<string, string | number> = {
Session_Key: this.merchantNo,
Start_Date: params.startDate,
End_Date: params.endDate,
}
if (params.searchKeyWord) requestData.Search_KeyWord = params.searchKeyWord
if (params.invoiceTypes) requestData.Invoice_Types = params.invoiceTypes
if (params.posSystemId) requestData.Pos_System_Id = params.posSystemId
if (params.pageNumber) requestData.Page_Number = params.pageNumber
return await this.callApi('List', requestData)
}
/**
* 呼叫 SmilePay API
*/
private async callApi(action: string, data: Record<string, any>): Promise<any> {
const url = `${SMILEPAY_API_URL}?Action=${action}`
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
})
const result = await response.json() as Record<string, any>
if (result.Status !== '1' && result.Status !== 1) {
throw new Error(`SmilePay API Error: ${result.Description || result.Message || JSON.stringify(result)}`)
}
return result
} catch (error) {
logger.error({ error, action }, 'SmilePay API 呼叫失敗')
throw error
}
}
private getCurrentDate(): string {
const now = new Date()
return `${now.getFullYear()}/${String(now.getMonth() + 1).padStart(2, '0')}/${String(now.getDate()).padStart(2, '0')}`
}
private getCurrentTime(): string {
const now = new Date()
return `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`
}
}
let einvoiceClient: EInvoiceClient | null = null
/**
* 取得電子發票客戶端實例
*/
export async function getEInvoiceClient(): Promise<EInvoiceClient> {
if (!einvoiceClient) {
const secrets = await getAppSecrets()
einvoiceClient = new EInvoiceClient(
secrets.smilepayMerchantNo || '',
secrets.smilepayHashKey || '',
secrets.smilepayHashIv || ''
)
}
return einvoiceClient
}