Skip to main content

เพิ่ม Placeorder

ต้องทำอะไรบ้างสำหรับการเพิ่ม Placeorder ?

  • รับข้อมูล order มาผ่าน request โดยจะรับ
    • Authorization ผ่าน header เข้ามาว่าเป็นสมาชิกหรือไม่ ? (ถ้าเป็น = save uid โดยการแกะจาก header)
    • รับ request body มาโดยจะรับเป็น products array ที่มีแค่ product_id และ quantity เท่านั้น (ราคาจริง เราจะมาคำนวนเองที่ Cloud function เพื่อความปลอดภัย)
    • ทำการ save ลง Firestore ใน collection orders เข้าไป
  • ใช้ feature ที่ชื่อ Transaction ใน Firestore
    • Transaction คือ feature ที่จะช่วยทำให้เราสามารถทำ Transaction แบบ Atomic และจะทำการ COMMIT เมื่อเสร็จสิ้น หรือ ROLLBACK ทุกคำสั่ง กลับไปเมื่อมีปัญหาได้ (ฟังเพิ่มเติมได้ในหัวข้อ Transaction ที่อยู่ในช่องเราเช่นกัน)

หน้าตาข้อมูลจะประมาณนี้

placeorder-01

รู้จักกับ Firebase admin

document ต้นฉบับ: https://firebase.google.com/docs/admin/setup

แน่นอน รอบนี้เราไม่ได้จัดการทุกอย่างผ่าน Frontend เหมือนเดิม library Firestore, Cloud Storage, Realtime Database ที่เคยใช้บน Vue จะไม่สามารถใช้งานผ่านการ import ด้วยวิธีเดิมได้

Firebase จึงได้เตรียม library เอาไว้คือ Firebase Admin SDK

Firebase Admin SDK คือ set ของ server libraries ที่สามารถทำให้ฝั่ง Bakcend สามารถตอบสนองกับ Firebase service ได้ โดย

  • สามารถอ่าน / เขียน Realtime Database data ด้วย full admin access ได้
  • generate และ verify Firebase token (ของ Firebase auth ของ Frontend) ได้
  • สามารถ access Google Cloud resourece ได้อย่าง Cloud Storage buckets, Cloud Firestore databases ที่เกี่ยวข้องกับ project ได้ ด้วย full admin access เช่นเดียวกัน

อย่างที่ Firebase Admin SDK เน้นย้ำ ทุกอย่างที่ run ใน Firebase admin นั้นจะถือว่าเป็น admin access

  • เท่ากับว่า จะไม่สนใจ security rule ที่มีการ setup เอาไว้เลย (จะทำการทะลุเป็นระดับ admin access)
  • cloud function จึงเหมาะสำหรับจัดการเรื่องของ Access Control ด้วยเช่นกัน ในกรณีที่เราต้องการจัดการเรื่องของ security และการ access data เอง

ทำการลง Firebase Admin SDK เข้า project ให้มาที่ folder ของ functions แล้วใช้คำสั่ง

npm install firebase-admin

เมื่อเรียบร้อย ให้ลองเพิ่มการเรียก firebase admin ที่ functions/index.js

  • โดยให้ลองเรียก initializeApp เพื่อทำการ init firebase admin application ขึ้นมาและใส่ ชื่อ project เข้าไป
const { onRequest } = require('firebase-functions/v2/https')
const { initializeApp } = require('firebase-admin/app')

exports.sayHello = onRequest(
(req, res) => {
initializeApp({
projectId: '<your project name>', // ชื่อ project ของตัวเอง
})
res.status(200).json({
message: 'Hello world!'
})
}
)

หากยังสามารถยิง API sayHello ออกมาได้โดยไม่เกิด error ออกมา = Firebase admin ทำงานอย่างถูกต้อง

  • ตามจริงแล้ว firebase admin นั้นต้องการ service-account.json ที่เป็นเหมือน key สำหรับการ access เข้า Firebase ด้วย
  • แต่เนื่องจากเรายัง run อยู่ที่ Firebase emulator อยู่ = ยังไม่จำเป็นต้องใช้ตอนนี้ (แต่เราจะใช้กันในบทสุดท้ายของ Project นี้)

ลง Express เพื่อให้ manage api project ง่ายขึ้น

document ต้นฉบับ: https://firebase.google.com/docs/hosting/functions

เนื่องจาก onRequest นั้นหากเราต้องการจัดการ method (ex. GET / POST) มันต้องจัดการ method ด้วยการ handle เองผ่าน request

เช่น ถ้าอยากให้ api sayHello ตัวเดิม รองรับเฉพาะ method POST = ก็ต้องสร้าง condition มาดักเอง

exports.sayHello = onRequest(
(req, res) => {
if (req.method === 'POST') {
res.status(200).json({
message: 'Hello world!'
})
} else {
res.status(405).send('Method Not Allowed')
}
}
)

ซึ่งมันจะไม่สะดวกเอามากๆ เมื่อ project เราเริ่มใหญ่ เราเลยจะทำการลง Library เสริมอีกตัวหนึ่งคือ express เข้ามา (Express เรามีการอธิบายไว้แล้วใน Web development 101 เรื่อง API หากใครยังไม่รู้จักตัวนี้สามารถไปดูก่อนได้)

วิธีลง Express

npm install express --save

หลังจากนั้นที่ cloud function case เดิม ให้เปลี่ยนมาเรียกแบบนี้แทน

const { onRequest } = require('firebase-functions/v2/https')
const express = require('express')
const app = express()

// ย้าย sayHello มาไว้ใน express
app.post('/sayHello', (req, res) => {
res.status(200).json({
message: 'Hello world!'
})
})

// path เริ่มต้นก็จะเป็น POST /api/sayHello ออกมาแทน
exports.api = onRequest(app)

ก็จะสามารถใช้ความสามารถ router ของ express ร่วมกับ cloud function ได้

ลง code ที่ cloud function กัน

Step ที่เราจะทำ

  • เพิ่ม config firebase-admin เข้าที่ functions/firebaseConfig.js แล้วให้ Export ตัวแปรออกมา
  • เพิิ่ม POST /api/placeorder สำหรับรับข้อมูล placeorder เข้ามาโดย request body จะหน้าตาเป็นแบบนี้
{
"source": "test_src", // จะปรับกันตอน omise รับอะไรมาก่อนก็ได้
"checkout": { // จำลองการส่งข้อมูลหน้าบ้าน
"email": "1",
"name": "2",
"address": "3",
"note": "4",
"products": [
{
"productId": "EtuytjyEpj0Kd1GOKBRK",
"quantity": 1
}
]
}
}

เพิ่ม functions/firebaseConfig.js

// import library ทั้งหมดผ่าน firebase-admin เข้ามา
const { initializeApp } = require('firebase-admin/app')
const { getFirestore } = require('firebase-admin/firestore')
const { getDatabase } = require('firebase-admin/database')
const { getAuth } = require('firebase-admin/auth')

initializeApp({
projectId: '<your project name>',
databaseURL: '<realtime database>' // realtime database url ถ้าใช้ emulator ต้องหยิบจาก emulator มา
})

const db = getFirestore()
const auth = getAuth()
const realtimeDB = getDatabase()

module.exports = {
db,
auth,
realtimeDB
}

ที่ functions/index.js

Note

  • เพิ่ม app.post('/placeorder') เข้ามาโดยให้เพิ่ม
    • เช็คว่าเป็น user ที่ login หรือไม่ ผ่าน header authorization ใช้คำสั่ง auth.verifyIdToken
    • เรียบร้อย ให้เขียนลง order collection ใน Firestore
  • ใช้ท่า transaction ไล่ลำดับมา
const { onRequest } = require('firebase-functions/v2/https')
const { onDocumentWritten } = require('firebase-functions/v2/firestore')

const express = require('express')

const app = express()

const { db, auth, realtimeDB } = require('./firebaseConfig.js')

app.post('/placeorder', async (req, res) => {
try {
const idToken = req.headers.authorization
const checkoutData = req.body.checkout
let userUid = ''
if (idToken) {
const decodedToken = await auth.verifyIdToken(idToken)
userUid = decodedToken.uid
}

// summary data
let summaryPrice = 0
let checkoutProducts = []
let omiseResponse = {}
let successOrderId = ''

// check stock, valid = decrease stock
await db.runTransaction(async (transaction) => {
for (const product of checkoutData.products) {
const productRef = db.collection('products').doc(product.productId)
const snapshot = await productRef.get()
const productData = snapshot.data()
const remainQuantity = productData.remainQuantity - product.quantity
if (remainQuantity < 0) {
throw new Error(`Product: ${productData.name} out of stock`)
}
summaryPrice += (productData.price * product.quantity)
transaction.update(productRef, { remainQuantity })

let checkoutProduct = product
checkoutProduct.price = productData.price
checkoutProduct.totalPrice = productData.price * product.quantity
checkoutProduct.name = productData.name
checkoutProduct.about = productData.about
checkoutProduct.imageUrl = productData.imageUrl
checkoutProducts.push(checkoutProduct)
}

const orderRef = db.collection('orders')

// create order id
const orderId = orderRef.doc().id

// create order
const orderData = {
chargeId: 'exam-charge-1234', // mock charge-id (ใช้ตอน omise)
email: checkoutData.email,
name: checkoutData.name,
address: checkoutData.address,
note: checkoutData.note || '',
totalPrice: summaryPrice,
paymentMethod: 'rabbit_linepay',
createdAt: new Date(),
products: checkoutProducts,
status: 'successful', // mock ให้เสร็จไปก่อน
orderId,
userUid
}

transaction.set(orderRef.doc(orderId), orderData)

successOrderId = orderId
})

res.json({
message: 'Order successful!',
redirectUrl: `http://localhost:5173?order_id=${successOrderId}`
})
} catch (error) {
console.log('error', error)
res.status(500).json({
message: error.message
})
}
})