diff --git a/docs/CHAPA_INTEGRATION.md b/docs/CHAPA_INTEGRATION.md
index 276a707..43da003 100644
--- a/docs/CHAPA_INTEGRATION.md
+++ b/docs/CHAPA_INTEGRATION.md
@@ -16,7 +16,7 @@ CHAPA_PUBLIC_KEY=CHAPUBK_TEST-xxxxxxxx
CHAPA_WEBHOOK_SECRET=your_webhook_secret_from_dashboard
CHAPA_BASE_URL=https://api.chapa.co/v1
CHAPA_CALLBACK_URL=https://your-api.example.com/api/v1/payments/chapa/callback
-CHAPA_RETURN_URL=https://your-app.example.com/payment/success
+CHAPA_RETURN_URL=https://your-api.example.com/payment/success
CHAPA_RECEIPT_URL=
```
@@ -29,9 +29,9 @@ Configure the same webhook URL in the Chapa dashboard:
1. Learner calls `POST /api/v1/subscriptions/checkout` or `POST /api/v1/payments/subscribe`.
2. Backend creates a pending payment and calls Chapa `POST /transaction/initialize`.
3. Client redirects the user to `payment_url` (`checkout_url` from Chapa).
-4. After payment, Chapa calls `callback_url` and sends a webhook.
-5. Backend verifies via `GET /transaction/verify/{tx_ref}` and activates the subscription.
-6. Client may poll `GET /api/v1/payments/verify/{tx_ref}` (`session_id` path param is the `tx_ref`).
+4. After payment, Chapa redirects the learner to `return_url` (`/payment/success`) and calls `callback_url`.
+5. The success page and callback both verify via Chapa `GET /transaction/verify/{tx_ref}` and activate the subscription when successful.
+6. Chapa also sends a webhook; client may poll `GET /api/v1/payments/verify/{tx_ref}` (`session_id` path param is the `tx_ref`).
## API Endpoints
@@ -41,7 +41,9 @@ Configure the same webhook URL in the Chapa dashboard:
| POST | `/api/v1/payments/subscribe` | Yes | Same as checkout |
| GET | `/api/v1/payments/verify/:session_id` | Yes | Verify by `tx_ref` |
| POST | `/api/v1/payments/webhook` | No | Chapa webhook (HMAC signature required) |
-| GET | `/api/v1/payments/chapa/callback` | No | Chapa redirect callback |
+| GET | `/api/v1/payments/chapa/callback` | No | Chapa server callback (JSON) |
+| GET | `/api/v1/payments/chapa/success` | No | Chapa learner success page (HTML) |
+| GET | `/payment/success` | No | Same HTML success page (`CHAPA_RETURN_URL`) |
| GET | `/api/v1/payments/methods` | No | Supported Chapa methods |
### Initiate payment request
diff --git a/internal/web_server/handlers/arifpay.go b/internal/web_server/handlers/arifpay.go
index b335b74..bd7c2b2 100644
--- a/internal/web_server/handlers/arifpay.go
+++ b/internal/web_server/handlers/arifpay.go
@@ -1,11 +1,9 @@
package handlers
import (
- "bytes"
"context"
"errors"
"fmt"
- "html/template"
"Yimaru-Backend/internal/domain"
"Yimaru-Backend/internal/services/chapa"
@@ -332,15 +330,7 @@ func (h *Handler) HandleArifpaySuccessPage(c *fiber.Ctx) error {
c.Query("nonce"),
)
- page := arifpaySuccessPageData{
- Title: "Subscription Payment Successful",
- Headline: "Your Yimaru Academy payment was received",
- Body: "Thank you for your payment. Your subscription is being activated and you can return to Yimaru Academy shortly.",
- BadgeLabel: "Payment successful",
- StatusLabel: "Activation in progress",
- ActionLabel: "Continue learning",
- ActionHref: "/",
- }
+ page := defaultPaymentSuccessPage()
if ref != "" {
payment, err := h.arifpaySvc.VerifyPayment(c.Context(), ref)
@@ -364,7 +354,7 @@ func (h *Handler) HandleArifpaySuccessPage(c *fiber.Ctx) error {
page.Helper = "Return to Yimaru Academy and refresh your subscription status if you do not see access immediately."
}
- html, err := renderArifpaySuccessPage(page)
+ html, err := renderPaymentSuccessPage(page)
if err != nil {
return c.Status(fiber.StatusInternalServerError).SendString("Failed to render success page")
}
@@ -585,95 +575,3 @@ func paymentToRes(p *domain.Payment) *paymentRes {
return res
}
-
-type arifpaySuccessPageData struct {
- Title string
- Headline string
- Body string
- Helper string
- BadgeLabel string
- StatusLabel string
- Reference string
- PlanName string
- ActionLabel string
- ActionHref string
-}
-
-func renderArifpaySuccessPage(data arifpaySuccessPageData) (string, error) {
- const tpl = `
-
-
-
-
- {{.Title}}
-
-
-
-
-
-
-
- |
- {{.BadgeLabel}}
- Yimaru Academy
- {{.Headline}}
- |
-
-
- |
- ✓
- {{.Body}}
- {{if .Helper}}{{.Helper}} {{end}}
-
-
- |
- Status
- {{.StatusLabel}}
- {{if .PlanName}}Plan: {{.PlanName}} {{end}}
- {{if .Reference}}Reference: {{.Reference}} {{end}}
- |
-
-
-
- |
-
-
- |
- Yimaru Academy subscription payments are verified securely before access is granted.
- |
-
-
- |
-
-
-
-`
-
- t, err := template.New("arifpay-success").Parse(tpl)
- if err != nil {
- return "", err
- }
- var buf bytes.Buffer
- if err := t.Execute(&buf, data); err != nil {
- return "", err
- }
- return buf.String(), nil
-}
-
-func firstNonEmpty(values ...string) string {
- for _, value := range values {
- if value != "" {
- return value
- }
- }
- return ""
-}
-
-func derefString(value *string) string {
- if value == nil {
- return ""
- }
- return *value
-}
diff --git a/internal/web_server/handlers/chapa.go b/internal/web_server/handlers/chapa.go
index 5ac8ed4..53a7731 100644
--- a/internal/web_server/handlers/chapa.go
+++ b/internal/web_server/handlers/chapa.go
@@ -99,6 +99,56 @@ func (h *Handler) HandleChapaCallback(c *fiber.Ctx) error {
})
}
+// HandleChapaSuccessPage godoc
+// @Summary Chapa payment success page
+// @Description Displays the Yimaru Academy success page after Chapa redirects the learner to return_url
+// @Tags payments
+// @Produce html
+// @Param trx_ref query string false "Chapa transaction reference (tx_ref)"
+// @Param tx_ref query string false "Chapa transaction reference"
+// @Param ref_id query string false "Chapa reference ID"
+// @Param status query string false "Payment status from Chapa redirect"
+// @Success 200 {string} string "HTML success page"
+// @Router /api/v1/payments/chapa/success [get]
+// @Router /payment/success [get]
+func (h *Handler) HandleChapaSuccessPage(c *fiber.Ctx) error {
+ txRef := firstNonEmpty(
+ c.Query("trx_ref"),
+ c.Query("tx_ref"),
+ )
+
+ page := defaultPaymentSuccessPage()
+
+ if txRef != "" {
+ payment, err := h.chapaSvc.VerifyPayment(c.Context(), txRef)
+ if err != nil {
+ h.logger.Warn("Failed to verify Chapa success redirect", "error", err, "tx_ref", txRef)
+ page.Body = "Thank you for your payment. We are confirming it with Chapa and will activate your subscription shortly."
+ page.Helper = "You can safely return to Yimaru Academy. If activation takes longer than expected, refresh the app in a moment."
+ page.Reference = txRef
+ } else {
+ page.Reference = txRef
+ page.PlanName = derefString(payment.PlanName)
+ if payment.Status == string(domain.PaymentStatusSuccess) {
+ page.StatusLabel = "Subscription active"
+ page.Body = "Your Yimaru Academy subscription is active. You now have access to your learning content."
+ } else {
+ page.Body = "Thank you for your payment. We received your success redirect and are finalizing subscription activation."
+ page.StatusLabel = "Processing confirmation"
+ }
+ }
+ } else {
+ page.Helper = "Return to Yimaru Academy and refresh your subscription status if you do not see access immediately."
+ }
+
+ html, err := renderPaymentSuccessPage(page)
+ if err != nil {
+ return c.Status(fiber.StatusInternalServerError).SendString("Failed to render success page")
+ }
+ c.Type("html", "utf-8")
+ return c.SendString(html)
+}
+
// GetChapaPaymentMethods godoc
// @Summary Get Chapa payment methods
// @Description Returns payment methods available on Chapa checkout
diff --git a/internal/web_server/handlers/payment_success_page.go b/internal/web_server/handlers/payment_success_page.go
new file mode 100644
index 0000000..475bd05
--- /dev/null
+++ b/internal/web_server/handlers/payment_success_page.go
@@ -0,0 +1,110 @@
+package handlers
+
+import (
+ "bytes"
+ "html/template"
+)
+
+type paymentSuccessPageData struct {
+ Title string
+ Headline string
+ Body string
+ Helper string
+ BadgeLabel string
+ StatusLabel string
+ Reference string
+ PlanName string
+ ActionLabel string
+ ActionHref string
+}
+
+func renderPaymentSuccessPage(data paymentSuccessPageData) (string, error) {
+ const tpl = `
+
+
+
+
+ {{.Title}}
+
+
+
+
+
+
+
+ |
+ {{.BadgeLabel}}
+ Yimaru Academy
+ {{.Headline}}
+ |
+
+
+ |
+ ✓
+ {{.Body}}
+ {{if .Helper}}{{.Helper}} {{end}}
+
+
+ |
+ Status
+ {{.StatusLabel}}
+ {{if .PlanName}}Plan: {{.PlanName}} {{end}}
+ {{if .Reference}}Reference: {{.Reference}} {{end}}
+ |
+
+
+
+ |
+
+
+ |
+ Yimaru Academy subscription payments are verified securely before access is granted.
+ |
+
+
+ |
+
+
+
+`
+
+ t, err := template.New("payment-success").Parse(tpl)
+ if err != nil {
+ return "", err
+ }
+ var buf bytes.Buffer
+ if err := t.Execute(&buf, data); err != nil {
+ return "", err
+ }
+ return buf.String(), nil
+}
+
+func defaultPaymentSuccessPage() paymentSuccessPageData {
+ return paymentSuccessPageData{
+ Title: "Subscription Payment Successful",
+ Headline: "Your Yimaru Academy payment was received",
+ Body: "Thank you for your payment. Your subscription is being activated and you can return to Yimaru Academy shortly.",
+ BadgeLabel: "Payment successful",
+ StatusLabel: "Activation in progress",
+ ActionLabel: "Continue learning",
+ ActionHref: "/",
+ }
+}
+
+func firstNonEmpty(values ...string) string {
+ for _, value := range values {
+ if value != "" {
+ return value
+ }
+ }
+ return ""
+}
+
+func derefString(value *string) string {
+ if value == nil {
+ return ""
+ }
+ return *value
+}
diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go
index e17fd61..9ca67f1 100644
--- a/internal/web_server/routes.go
+++ b/internal/web_server/routes.go
@@ -274,7 +274,9 @@ func (a *App) initAppRoutes() {
groupV1.Post("/payments/:id/cancel", a.authMiddleware, a.RequirePermission("payments.cancel"), h.CancelPayment)
groupV1.Post("/payments/webhook", h.HandleChapaWebhook)
groupV1.Get("/payments/arifpay/success", h.HandleArifpaySuccessPage)
+ groupV1.Get("/payments/chapa/success", h.HandleChapaSuccessPage)
groupV1.Get("/payments/chapa/callback", h.HandleChapaCallback)
+ a.fiber.Get("/payment/success", h.HandleChapaSuccessPage)
// Direct Payments
groupV1.Post("/payments/direct", a.authMiddleware, a.RequirePermission("payments.direct_initiate"), h.InitiateDirectPayment)