Yimaru-BackEnd/internal/web_server/app.go
Yared Yemane 1f7b38861e Integrate Chapa for learner subscription payments
Add Chapa checkout, verify, webhook, and callback flows so subscriptions activate only after confirmed payment. Route subscription checkout through Chapa while keeping ArifPay for direct payments. Include integration docs and a Postman collection.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-21 03:35:57 -07:00

255 lines
7.6 KiB
Go

package httpserver
import (
dbgen "Yimaru-Backend/gen/db"
"Yimaru-Backend/internal/config"
activitylogservice "Yimaru-Backend/internal/services/activity_log"
"Yimaru-Backend/internal/services/arifpay"
"Yimaru-Backend/internal/services/chapa"
"Yimaru-Backend/internal/services/assessment"
"Yimaru-Backend/internal/services/authentication"
cloudconvertservice "Yimaru-Backend/internal/services/cloudconvert"
"Yimaru-Backend/internal/services/courses"
"Yimaru-Backend/internal/services/examprep"
"Yimaru-Backend/internal/services/faqs"
issuereporting "Yimaru-Backend/internal/services/issue_reporting"
"Yimaru-Backend/internal/services/lessons"
"Yimaru-Backend/internal/services/lmsprogress"
minioservice "Yimaru-Backend/internal/services/minio"
"Yimaru-Backend/internal/services/modules"
notificationservice "Yimaru-Backend/internal/services/notification"
"Yimaru-Backend/internal/services/personas"
"Yimaru-Backend/internal/services/practices"
"Yimaru-Backend/internal/services/programs"
"Yimaru-Backend/internal/services/questions"
ratingsservice "Yimaru-Backend/internal/services/ratings"
rbacservice "Yimaru-Backend/internal/services/rbac"
"Yimaru-Backend/internal/services/recommendation"
"Yimaru-Backend/internal/services/subscriptions"
"Yimaru-Backend/internal/services/team"
vimeoservice "Yimaru-Backend/internal/services/vimeo"
"Yimaru-Backend/internal/services/settings"
"Yimaru-Backend/internal/services/transaction"
"Yimaru-Backend/internal/services/user"
jwtutil "Yimaru-Backend/internal/web_server/jwt"
customvalidator "Yimaru-Backend/internal/web_server/validator"
"context"
"fmt"
"log/slog"
"time"
"go.uber.org/zap"
"github.com/bytedance/sonic"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cors"
)
type App struct {
assessmentSvc *assessment.Service
questionsSvc *questions.Service
faqSvc *faqs.Service
personaSvc *personas.Service
examPrepSvc *examprep.Service
programSvc *programs.Service
courseSvc *courses.Service
moduleSvc *modules.Service
lessonSvc *lessons.Service
lmsProgressSvc *lmsprogress.Service
practiceSvc *practices.Service
subscriptionsSvc *subscriptions.Service
arifpaySvc *arifpay.ArifpayService
chapaSvc *chapa.Service
issueReportingSvc *issuereporting.Service
vimeoSvc *vimeoservice.Service
teamSvc *team.Service
activityLogSvc *activitylogservice.Service
cloudConvertSvc *cloudconvertservice.Service
minioSvc *minioservice.Service
ratingSvc *ratingsservice.Service
fiber *fiber.App
recommendationSvc recommendation.RecommendationService
cfg *config.Config
logger *slog.Logger
NotidicationStore *notificationservice.Service
port int
settingSvc *settings.Service
authSvc *authentication.Service
userSvc *user.Service
transactionSvc *transaction.Service
validator *customvalidator.CustomValidator
JwtConfig jwtutil.JwtConfig
Logger *slog.Logger
mongoLoggerSvc *zap.Logger
analyticsDB *dbgen.Queries
rbacSvc *rbacservice.Service
stopPurgeWorker context.CancelFunc
}
func NewApp(
assessmentSvc *assessment.Service,
questionsSvc *questions.Service,
faqSvc *faqs.Service,
personaSvc *personas.Service,
examPrepSvc *examprep.Service,
programSvc *programs.Service,
courseSvc *courses.Service,
moduleSvc *modules.Service,
lessonSvc *lessons.Service,
lmsProgressSvc *lmsprogress.Service,
practiceSvc *practices.Service,
subscriptionsSvc *subscriptions.Service,
arifpaySvc *arifpay.ArifpayService,
chapaSvc *chapa.Service,
issueReportingSvc *issuereporting.Service,
vimeoSvc *vimeoservice.Service,
teamSvc *team.Service,
activityLogSvc *activitylogservice.Service,
cloudConvertSvc *cloudconvertservice.Service,
minioSvc *minioservice.Service,
ratingSvc *ratingsservice.Service,
port int, validator *customvalidator.CustomValidator,
settingSvc *settings.Service,
authSvc *authentication.Service,
logger *slog.Logger,
JwtConfig jwtutil.JwtConfig,
userSvc *user.Service,
transactionSvc *transaction.Service,
notidicationStore *notificationservice.Service,
recommendationSvc recommendation.RecommendationService,
cfg *config.Config,
mongoLoggerSvc *zap.Logger,
analyticsDB *dbgen.Queries,
rbacSvc *rbacservice.Service,
) *App {
app := fiber.New(fiber.Config{
CaseSensitive: true,
DisableHeaderNormalizing: true,
JSONEncoder: sonic.Marshal,
JSONDecoder: sonic.Unmarshal,
BodyLimit: 500 * 1024 * 1024, // 500 MB
})
app.Use(cors.New(cors.Config{
AllowOrigins: "*",
AllowMethods: "GET,POST,PUT,PATCH,DELETE,OPTIONS",
AllowHeaders: "Content-Type,Authorization,platform",
// AllowCredentials: true,
}))
app.Static("/static", "./static")
s := &App{
assessmentSvc: assessmentSvc,
questionsSvc: questionsSvc,
faqSvc: faqSvc,
personaSvc: personaSvc,
examPrepSvc: examPrepSvc,
programSvc: programSvc,
courseSvc: courseSvc,
moduleSvc: moduleSvc,
lessonSvc: lessonSvc,
lmsProgressSvc: lmsProgressSvc,
practiceSvc: practiceSvc,
subscriptionsSvc: subscriptionsSvc,
arifpaySvc: arifpaySvc,
chapaSvc: chapaSvc,
vimeoSvc: vimeoSvc,
teamSvc: teamSvc,
activityLogSvc: activityLogSvc,
cloudConvertSvc: cloudConvertSvc,
minioSvc: minioSvc,
ratingSvc: ratingSvc,
issueReportingSvc: issueReportingSvc,
fiber: app,
port: port,
settingSvc: settingSvc,
authSvc: authSvc,
validator: validator,
logger: logger,
JwtConfig: JwtConfig,
userSvc: userSvc,
transactionSvc: transactionSvc,
NotidicationStore: notidicationStore,
Logger: logger,
recommendationSvc: recommendationSvc,
cfg: cfg,
mongoLoggerSvc: mongoLoggerSvc,
analyticsDB: analyticsDB,
rbacSvc: rbacSvc,
}
s.initAppRoutes()
return s
}
func (a *App) Run() error {
a.startAccountDeletionPurgeWorker()
defer a.stopAccountDeletionPurgeWorker()
return a.fiber.Listen(fmt.Sprintf(":%d", a.port))
}
func (a *App) startAccountDeletionPurgeWorker() {
if a.cfg == nil || !a.cfg.AccountDeletionPurgeEnabled {
a.logger.Info("account deletion purge worker disabled")
return
}
interval := a.cfg.AccountDeletionPurgeInterval
if interval <= 0 {
interval = time.Hour
}
batchSize := a.cfg.AccountDeletionPurgeBatchSize
if batchSize <= 0 {
batchSize = 100
}
ctx, cancel := context.WithCancel(context.Background())
a.stopPurgeWorker = cancel
a.logger.Info(
"starting account deletion purge worker",
"interval", interval.String(),
"batch_size", batchSize,
)
go func() {
// Run once on startup so stale due rows are cleaned quickly.
a.runAccountDeletionPurgeOnce(ctx, batchSize)
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
a.logger.Info("account deletion purge worker stopped")
return
case <-ticker.C:
a.runAccountDeletionPurgeOnce(ctx, batchSize)
}
}
}()
}
func (a *App) stopAccountDeletionPurgeWorker() {
if a.stopPurgeWorker != nil {
a.stopPurgeWorker()
}
}
func (a *App) runAccountDeletionPurgeOnce(ctx context.Context, batchSize int32) {
deletedCount, err := a.userSvc.PurgeDueUserDeletions(ctx, batchSize)
if err != nil {
a.logger.Error("account deletion purge run failed", "error", err)
return
}
if deletedCount > 0 {
a.logger.Info("account deletion purge run completed", "deleted_count", deletedCount, "batch_size", batchSize)
}
}