ข้ามไปยังเนื้อหาหลัก

การอนุมัติล่วงหน้า (Auth & Capture)

อนุมัติการชำระเงินของลูกค้าล่วงหน้าและจับเงินในภายหลังโดยใช้ขั้นตอนการชำระเงินแบบ 2 ขั้นของ Omise เพื่อให้ได้ความยืดหยุ่นในการจับเวลาการชำระเงินและการปฏิบัติตามคำสั่งซื้อ

ภาพรวม

การอนุมัติล่วงหน้า (เรียกอีกอย่างว่า "Auth และ Capture") เป็นกระบวนการชำระเงินแบบ 2 ขั้นตอนซึ่งคุณต้องอนุมัติการชำระเงินเพื่อตรวจสอบความพร้อมของเงินและจับมันไว้ก่อน จากนั้นจึงจับการชำระเงินจริงในภายหลังเมื่อคุณพร้อมที่จะชำระเงิน สิ่งนี้มีประโยชน์สำหรับผลิตภัณฑ์ที่จัดทำตามคำสั่ง การจองโรงแรม บริการเช่า และสถานการณ์ใด ๆ ที่การปฏิบัติตามคำสั่งซื้อเกิดขึ้นหลังจากการสั่งซื้อ

แนวคิดหลัก:

  • Authorize - ตรวจสอบการ์ดและจับเงิน (ยังไม่ได้เรียกเก็บเงินจากลูกค้า)
  • Capture - รวบรวมเงินที่จับมาไว้ (เรียกเก็บเงินจากลูกค้า)
  • Void - ยกเลิกการอนุมัติก่อน Capture (เพื่อให้เงินถูกปล่อยออกมา)
  • Auto-capture - โหมดเริ่มต้น โดยจะ Authorize และ Capture ทันทีกันเลย
  • Manual capture - กระบวนการแบบ 2 ขั้นตอน ต้องการการ Capture อย่างชัดแจ้ง

เมื่อจะใช้การอนุมัติล่วงหน้า

ยูสเคสที่ดี

ผลิตภัณฑ์ที่จัดทำตามคำสั่ง

  • Authorize เมื่อมีการสั่งซื้อ
  • Capture เมื่อมีการส่งสินค้า
  • Void หากสินค้าไม่พร้อม

การจองโรงแรม

  • Authorize เมื่อมีการจอง
  • Capture เมื่อเช็คเอาต์
  • ปรับจำนวนเงินสำหรับบริการเสริม

บริการเช่า

  • Authorize เงินประกัน
  • Capture ค่าเสียหายหรือค่าธรรมเนียม
  • ปล่อยถ้าไม่มีปัญหา

บริการที่กำหนดเอง

  • Authorize ต้นทุนโดยประมาณ
  • Capture ต้นทุนจริงสุดท้าย
  • บัญชีสำหรับการเปลี่ยนแปลงขอบเขต

การตรวจสอบสินค้าคงคลัง

  • Authorize ทันที
  • ตรวจสอบความพร้อมของสินค้าคงคลัง
  • Capture ถ้ามีสินค้า Void หากไม่มี

ไม่แนะนำ

สินค้าดิจิทัล - ส่งมอบทันที ใช้ Auto-capture ❌ การสมัครสมาชิก - ใช้การเรียกเก็บเงินปกติ (ไม่สามารถ Pre-auth ซ้ำ ๆ ได้) ❌ สินค้าคุณค่าต่ำ - ความซับซ้อนที่เพิ่มเติมไม่มูลค่า ❌ การปฏิบัติตามคำสั่งซื้อทันที - ใช้ Auto-capture

วิธีการทำงาน

การใช้งาน

ขั้นตอนที่ 1: สร้างการอนุมัติ

curl https://api.omise.co/charges \
-u skey_test_YOUR_SECRET_KEY: \
-d "amount=100000" \
-d "currency=THB" \
-d "card=tokn_test_..." \
-d "capture=false"

การตอบสนอง:

{
"object": "charge",
"id": "chrg_test_5rt6s9vah5lkvi1rh9c",
"amount": 100000,
"currency": "THB",
"status": "pending",
"authorized": true,
"paid": false,
"capture": false,
"capturable": true,
"expires_at": "2025-02-13T00:00:00Z"
}

ขั้นตอนที่ 2: จับเงิน

// หลังจากการปฏิบัติตามคำสั่งซื้อแล้ว
const capture = await omise.charges.capture('chrg_test_5rt6s9vah5lkvi1rh9c');

console.log('สถานะ Capture:', capture.status); // 'successful'
console.log('Paid:', capture.paid); // true

จับเงินด้วยจำนวนเงินที่แตกต่างกัน (ถ้าได้รับการสนับสนุน):

// จับเงินน้อยกว่าที่ได้รับการอนุมัติ (เช่น บิลสุดท้ายต่ำกว่า)
const capture = await omise.charges.capture('chrg_test_...', {
capture_amount: 80000 // จับเฉพาะ ฿800 ของ ฿1,000 ที่ได้รับการอนุมัติ
});

ขั้นตอนที่ 3: หรือยกเลิก/ยืมการอนุมัติ

// ยกเลิกการอนุมัติหากคำสั่งซื้อถูกยกเลิก
const reversed = await omise.charges.reverse('chrg_test_5rt6s9vah5lkvi1rh9c');

console.log('สถานะ:', reversed.status); // 'reversed' หรือ 'expired'
console.log('ยกเลิกเมื่อ:', reversed.reversed_at);

ตัวอย่างที่สมบูรณ์

const express = require('express');
const omise = require('omise')({
secretKey: process.env.OMISE_SECRET_KEY
});

const app = express();
app.use(express.json());

// ขั้นตอนที่ 1: ลูกค้าวางสั่งซื้อ
app.post('/checkout', async (req, res) => {
try {
const { tokenId, amount, orderId } = req.body;

// อนุมัติการชำระเงิน (ไม่จับเงินเลย)
const charge = await omise.charges.create({
amount: amount,
currency: 'THB',
card: tokenId,
capture: false,
description: `Pre-auth for Order #${orderId}`,
metadata: {
order_id: orderId,
authorized_at: new Date().toISOString()
}
});

if (charge.status === 'pending' && charge.authorized) {
// บันทึกลงในฐานข้อมูล
await db.orders.create({
order_id: orderId,
charge_id: charge.id,
status: 'authorized',
amount: amount,
expires_at: charge.expires_at
});

res.json({
success: true,
message: 'Payment authorized',
order_id: orderId
});
} else {
res.status(400).json({
error: 'Authorization failed',
reason: charge.failure_message
});
}

} catch (error) {
res.status(500).json({ error: error.message });
}
});

// ขั้นตอนที่ 2: ปฏิบัติตามคำสั่งซื้อและจับเงิน
app.post('/orders/:orderId/ship', async (req, res) => {
try {
const { orderId } = req.params;
const { trackingNumber } = req.body;

// รับคำสั่งซื้อ
const order = await db.orders.findOne({ order_id: orderId });

if (order.status !== 'authorized') {
return res.status(400).json({
error: 'Order not in authorized state'
});
}

// จับเงิน
const capture = await omise.charges.capture(order.charge_id);

if (capture.status === 'successful') {
// อัปเดตคำสั่งซื้อ
await db.orders.update({
order_id: orderId,
status: 'captured',
shipped: true,
tracking_number: trackingNumber,
captured_at: new Date()
});

res.json({
success: true,
message: 'Payment captured, order shipped'
});
} else {
res.status(400).json({
error: 'Capture failed'
});
}

} catch (error) {
res.status(500).json({ error: error.message });
}
});

// ขั้นตอนที่ 3: ยกเลิกคำสั่งซื้อและปล่อยเงิน
app.post('/orders/:orderId/cancel', async (req, res) => {
try {
const { orderId } = req.params;
const { reason } = req.body;

const order = await db.orders.findOne({ order_id: orderId });

if (order.status !== 'authorized') {
return res.status(400).json({
error: 'Order cannot be canceled'
});
}

// ยกเลิกการอนุมัติ
const reversed = await omise.charges.reverse(order.charge_id);

// อัปเดตคำสั่งซื้อ
await db.orders.update({
order_id: orderId,
status: 'canceled',
cancellation_reason: reason,
canceled_at: new Date()
});

res.json({
success: true,
message: 'Authorization reversed, funds released'
});

} catch (error) {
res.status(500).json({ error: error.message });
}
});

// งานพื้นหลัง: ยกเลิกการอนุมัติที่หมดอายุโดยอัตโนมัติ
cron.schedule('0 * * * *', async () => {
const expiredOrders = await db.orders.find({
status: 'authorized',
expires_at: { $lt: new Date() }
});

for (const order of expiredOrders) {
try {
await omise.charges.reverse(order.charge_id);
await db.orders.update({
order_id: order.order_id,
status: 'expired'
});
console.log(`Reversed expired order: ${order.order_id}`);
} catch (error) {
console.error(`Failed to reverse ${order.order_id}:`, error);
}
}
});

app.listen(3000);

การอนุมัติหมดอายุ

การอนุมัติหมดอายุหากไม่จับเงิน:

Network การ์ดระยะเวลาหมดอายุ
Visa7 วัน
Mastercard7 วัน
Amex7 วัน
JCB7 วัน
Auto-Expiry

การอนุมัติที่ไม่ได้จับเงินจะหมดอายุโดยอัตโนมัติหลังจากระยะเวลาที่กำหนดของเครือข่ายการ์ด ตรวจสอบวันที่หมดอายุและจับเงินก่อนการหมดอายุ

// ตรวจสอบว่าการอนุมัติกำลังจะหมดอายุหรือไม่
function isExpiringSoon(charge) {
const expiryDate = new Date(charge.expires_at);
const now = new Date();
const daysUntilExpiry = (expiryDate - now) / (1000 * 60 * 60 * 24);

return daysUntilExpiry < 2; // น้อยกว่า 2 วัน
}

// แจ้งเตือนหากกำลังจะหมดอายุ
if (isExpiringExpiringSoon(charge)) {
await sendExpiryAlert(order.id, charge.expires_at);
}

การปรับจำนวนเงิน Capture

สถานการณ์บางอย่างช่วยให้สามารถจับเงินจำนวนต่างกันได้:

จับเงินน้อยลง (การปรับลดลง)

// ได้รับการอนุมัติ ฿1,000 แต่ต้นทุนสุดท้ายคือ ฿800
const capture = await omise.charges.capture('chrg_test_...', {
capture_amount: 80000 // ฿800 แทน ฿1,000
});

ยูสเคส:

  • บิลสุดท้ายต่ำกว่าค่าประมาณการ
  • ลูกค้ากลับสินค้าบางชิ้นก่อนการจัดส่ง
  • นำไปใช้ส่วนลดแบบโปรโมชั่น
  • ตรวจพบความเสียหายหรือข้อบกพร่อง

จับเงินมากขึ้น (ไม่รองรับ)

Cannot Capture More

คุณไม่สามารถจับเงินมากกว่าจำนวนเงินที่ได้รับการอนุมัติ หากต้นทุนสุดท้ายสูงกว่า คุณต้อง:

  1. สร้างการชำระเงินใหม่สำหรับความแตกต่าง หรือ
  2. ยกเลิกการอนุมัติดั้งเดิมและสร้างการชำระเงินใหม่สำหรับจำนวนเงินที่ถูกต้อง

แนวทางการใช้งานที่ดีที่สุด

1. ตรวจสอบการหมดอายุของการอนุมัติ

// งาน cron ประจำวัน
cron.schedule('0 8 * * *', async () => {
// ค้นหาการอนุมัติที่จะหมดอายุใน 2 วัน
const expiringCharges = await db.orders.find({
status: 'authorized',
expires_at: {
$gte: new Date(),
$lt: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000)
}
});

for (const order of expiringCharges) {
// แจ้งเตือนทีมการปฏิบัติตาม
await sendSlackNotification({
message: `⚠️ Order ${order.order_id} authorization expires in 2 days!`,
urgency: 'high'
});

// ส่งอีเมลถึงลูกค้าเกี่ยวกับการล่าช้า
await sendEmailToCustomer(order.customer_email, {
template: 'order_delay',
order_id: order.order_id
});
}
});

2. การสื่อสารที่ชัดเจน

// เมื่อทำการอนุมัติ
await sendEmail({
to: customer.email,
subject: 'Order Confirmed - Payment Authorized',
html: `
<h2>Order #${orderId} Confirmed</h2>
<p>We've authorized your payment of ฿${amount / 100}.</p>
<p><strong>Important:</strong> Your card will only be charged when your order ships.</p>
<ul>
<li>Estimated ship date: ${estimatedShipDate}</li>
<li>Authorization expires: ${expiresAt}</li>
</ul>
<p>You'll receive a confirmation email when your card is charged.</p>
`
});

3. จับเงินโดยอัตโนมัติเมื่อจัดส่ง

// รวมเข้ากับระบบการจัดส่ง
app.post('/webhooks/shipstation', async (req, res) => {
const shipment = req.body;

if (shipment.event === 'shipment_created') {
const order = await db.orders.findOne({
tracking_number: shipment.tracking_number
});

if (order && order.status === 'authorized') {
// จับเงินโดยอัตโนมัติเมื่อจัดส่ง
await omise.charges.capture(order.charge_id);

await db.orders.update({
order_id: order.order_id,
status: 'captured',
captured_at: new Date()
});

// แจ้งให้ลูกค้าทราบ
await sendShippedEmail(order.customer_email, {
tracking: shipment.tracking_number,
amount: order.amount
});
}
}

res.sendStatus(200);
});

4. จัดการการปฏิบัติตามบางส่วน

async function handlePartialShipment(orderId, shippedItems) {
const order = await db.orders.findOne({ order_id: orderId });

// คำนวณจำนวนเงินสำหรับสินค้าที่จัดส่ง
const shippedAmount = shippedItems.reduce((sum, item) => {
return sum + (item.price * item.quantity);
}, 0);

// จับเงินเฉพาะสำหรับสินค้าที่จัดส่ง
await omise.charges.capture(order.charge_id, {
capture_amount: shippedAmount
});

// สร้างการอนุมัติใหม่สำหรับสินค้าที่เหลือ
const remainingAmount = order.amount - shippedAmount;
if (remainingAmount > 0) {
const newCharge = await omise.charges.create({
amount: remainingAmount,
currency: 'THB',
customer: order.customer_id,
capture: false
});

await db.orders.create({
parent_order_id: orderId,
charge_id: newCharge.id,
status: 'authorized',
amount: remainingAmount
});
}
}

5. ระยะเวลาพิเศษสำหรับการตรวจสอบสต็อก

async function processOrder(orderId) {
const order = await db.orders.findOne({ order_id: orderId });

// ตรวจสอบสต็อก
const inStock = await checkInventory(order.items);

if (inStock) {
// จับเงินทันทีหากมีในสต็อก
await omise.charges.capture(order.charge_id);
await fulfillOrder(orderId);
} else {
// ให้ 24 ชั่วโมงในการเก็บสต็อก
setTimeout(async () => {
const stillInStock = await checkInventory(order.items);

if (stillInStock) {
await omise.charges.capture(order.charge_id);
await fulfillOrder(orderId);
} else {
// ยกเลิกและคืนเงิน
await omise.charges.reverse(order.charge_id);
await notifyCustomer(order.customer_email, 'out_of_stock');
}
}, 24 * 60 * 60 * 1000); // 24 ชั่วโมง
}
}

การทดสอบ

โหมดการทดสอบการอนุมัติล่วงหน้า

ใช้การ์ดทดสอบเพื่อตรวจสอบการใช้งานการอนุมัติล่วงหน้าของคุณ:

const omise = require('omise')({
secretKey: 'skey_test_YOUR_SECRET_KEY'
});

// การทดสอบการอนุมัติ
const charge = await omise.charges.create({
amount: 100000,
currency: 'THB',
card: 'tokn_test_5rt6s9vah5lkvi1rh9c', // โทเค็นการ์ดทดสอบ
capture: false,
metadata: {
test_scenario: 'pre_authorization'
}
});

console.log('สถานะ:', charge.status); // 'pending'
console.log('Authorized:', charge.authorized); // true
console.log('Capturable:', charge.capturable); // true

สถานการณ์การทดสอบ

1. การอนุมัติและจับเงินที่สำเร็จ

// ขั้นตอนที่ 1: อนุมัติ
const auth = await omise.charges.create({
amount: 100000,
currency: 'THB',
card: 'tokn_test_4242', // การ์ดสำเร็จ
capture: false
});

console.log('Authorization successful:', auth.status === 'pending');

// ขั้นตอนที่ 2: จับเงิน
const capture = await omise.charges.capture(auth.id);
console.log('Capture successful:', capture.status === 'successful');
console.log('ลูกค้า charged:', capture.paid); // true

2. การอนุมัติและยกเลิก

// อนุมัติ
const auth = await omise.charges.create({
amount: 100000,
currency: 'THB',
card: 'tokn_test_4242',
capture: false
});

// ยกเลิกการอนุมัติ
const reversed = await omise.charges.reverse(auth.id);
console.log('Voided:', reversed.status === 'reversed');

3. จับเงินบางส่วน

// อนุมัติ ฿1,000
const auth = await omise.charges.create({
amount: 100000,
currency: 'THB',
card: 'tokn_test_4242',
capture: false
});

// จับเฉพาะ ฿800
const partial = await omise.charges.capture(auth.id, {
capture_amount: 80000
});
console.log('Captured amount:', partial.amount); // 80000

4. การอนุมัติที่กำลังหมดอายุ

// สร้างการอนุมัติ
const auth = await omise.charges.create({
amount: 100000,
currency: 'THB',
card: 'tokn_test_4242',
capture: false
});

// ตรวจสอบวันที่หมดอายุ
console.log('Expires at:', auth.expires_at);

// จำลองการหมดอายุ (อย่าจับเงินก่อนการหมดอายุ)
// ในโหมดทดสอบ charges จะถูกทำเครื่องหมายว่าหมดอายุหลังจาก 7 วัน

การ์ดทดสอบสำหรับการอนุมัติล่วงหน้า

เลขการ์ดการอนุมัติจับเงินผลลัพธ์
4242 4242 4242 4242สำเร็จสำเร็จขั้นตอนเต็มทำงาน
4000 0000 0000 0002ปฏิเสธN/Aการอนุมัติล้มเหลว
4242 4242 4242 4242สำเร็จ(ไม่จับเงิน)หมดอายุโดยอัตโนมัติหลังจาก 7 วัน

การทดสอบผ่าน Dashboard

  1. สร้างการอนุมัติ ในโหมดทดสอบ
  2. ไปที่ Dashboard → Charges
  3. ค้นหา charge ที่กำลังดำเนินการ (สถานะ: pending)
  4. คลิกเมนู Actions dropdown:
    • "Capture" - จำลองการจับเงินที่สำเร็จ
    • "Reverse" - จำลองการยกเลิก

การทดสอบ Webhooks

app.post('/webhooks/omise', (req, res) => {
const event = req.body;

switch (event.key) {
case 'charge.create':
// สร้างการอนุมัติ
console.log('Auth created:', event.data.capture); // false
break;

case 'charge.capture':
// จับเงินการอนุมัติ
console.log('Captured:', event.data.paid); // true
break;

case 'charge.reverse':
// ยกเลิกการอนุมัติ
console.log('Reversed:', event.data.status); // 'reversed'
break;

case 'charge.expire':
// การอนุมัติหมดอายุ (ไม่จับเงินตรงเวลา)
console.log('Expired:', event.data.status); // 'expired'
break;
}

res.sendStatus(200);
});

การทดสอบกรณีข้อผิดพลาด

// ทดสอบ: ไม่สามารถจับเงินมากกว่าที่ได้รับการอนุมัติ
try {
const auth = await omise.charges.create({
amount: 100000,
capture: false
});

// สิ่งนี้ควรล้มเหลว
await omise.charges.capture(auth.id, {
capture_amount: 150000 // มากกว่าที่ได้รับการอนุมัติ
});
} catch (error) {
console.log('Expected error:', error.message);
}

// ทดสอบ: ไม่สามารถจับเงินที่จับไปแล้ว
try {
const auth = await omise.charges.create({
amount: 100000,
capture: false
});

await omise.charges.capture(auth.id);
await omise.charges.capture(auth.id); // การจับครั้งที่สองควรล้มเหลว
} catch (error) {
console.log('Expected error:', error.message);
}

// ทดสอบ: ไม่สามารถจับเงินการอนุมัติที่ยกเลิกแล้ว
try {
const auth = await omise.charges.create({
amount: 100000,
capture: false
});

await omise.charges.reverse(auth.id);
await omise.charges.capture(auth.id); // ควรล้มเหลว
} catch (error) {
console.log('Expected error:', error.message);
}

คำถามที่พบบ่อย

อะไรคือความแตกต่างระหว่างการอนุมัติล่วงหน้าและการชำระเงินปกติ?
  • การชำระเงินปกติ (auto-capture): อนุมัติและจับเงินทันทีในขั้นตอนเดียว ลูกค้าจะถูกเรียกเก็บเงินทันที
  • การอนุมัติล่วงหน้า (manual capture): อนุมัติก่อน (จับเงินไว้) จับเงินหลังจาก (เรียกเก็บเงินลูกค้า) สองขั้นตอนแยกต่างหาก
นานเท่าไหร่ที่ฉันสามารถถือครองการอนุมัติ?

การถือครองการอนุมัติอยู่ 7 วัน สำหรับการ์ดส่วนใหญ่ หลังจากนั้น พวกเขาจะหมดอายุโดยอัตโนมัติและเงินจะถูกปล่อยกลับไปยังลูกค้า

ฉันสามารถจับเงินมากกว่าจำนวนเงินที่ได้รับการอนุมัติได้หรือไม่?

ไม่ได้ คุณสามารถจับเงินได้มากที่สุดเท่าจำนวนที่ได้รับการอนุมัติ ในการเรียกเก็บเงินเพิ่มเติม:

  1. สร้างการชำระเงินแยกต่างหากสำหรับความแตกต่าง หรือ
  2. ยกเลิกการอนุมัติดั้งเดิมและสร้างรายการใหม่สำหรับจำนวนเต็ม
จะเกิดอะไรขึ้นหากฉันไม่จับเงินก่อนหมดอายุ?

การอนุมัติหมดอายุโดยอัตโนมัติและยกเลิก เงินจะถูกปล่อยกลับไปยังลูกค้า คุณไม่สามารถจับเงินหลังจากหมดอายุได้

ฉันสามารถจับเงินบางส่วนจากการอนุมัติได้หรือไม่?

ได้ คุณสามารถจับเงินน้อยกว่าจำนวนที่ได้รับการอนุมัติหากต้นทุนสุดท้ายต่ำกว่า จำนวนเงินที่เหลือจะถูกปล่อยโดยอัตโนมัติ

ลูกค้าเห็นการเรียกเก็บเงินที่รอดำเนินการหรือไม่?

ใช่ ธนาคารส่วนใหญ่แสดงธุรกรรม "pending" หรือ "authorized" ในการธนาคารออนไลน์ ลูกค้าจะไม่ถูกเรียกเก็บเงินจนกว่าคุณจะจับเงิน

ฉันสามารถใช้การอนุมัติล่วงหน้ากับ 3D Secure ได้หรือไม่?

ได้ 3D Secure ทำงานร่วมกับการอนุมัติล่วงหน้า ลูกค้าตรวจสอบสิทธิ์ระหว่างการอนุมัติ จากนั้นคุณจึงจับเงินหลังจากที่พร้อม

แหล่งข้อมูลที่เกี่ยวข้อง

ขั้นตอนถัดไป

  1. สร้างการอนุมัติ
  2. นำไปใช้งานตรรมชาติของการจับเงิน
  3. ตั้งค่าการตรวจสอบการหมดอายุ
  4. ทดสอบขั้นตอน
  5. รวมเข้ากับการปฏิบัติตาม
  6. เข้าสู่โหมดจริง