From 4f5950543e6628fc66a056c5cfa9dbe8f0a0003d Mon Sep 17 00:00:00 2001 From: "aditya.siregar" Date: Fri, 18 Jul 2025 20:10:29 +0700 Subject: [PATCH] init --- .air.toml | 44 + .dockerignore | 87 +- ANALYTICS_API.md | 292 ++ DOCKER.md | 312 ++ Dockerfile | 93 +- Dockerfile copy | 24 - Makefile | 12 +- ORDER_VOID_STATUS_IMPROVEMENT.md | 120 + OUTLET_TAX_CALCULATION.md | 155 + PRODUCT_STOCK_MANAGEMENT.md | 157 + PRODUCT_VARIANT_PRICE_MODIFIER.md | 127 + PROFIT_LOSS_ANALYTICS_API.md | 241 ++ README.md | 250 +- cmd/server/main.go | 30 + config/brevo.go | 9 - config/configs.go | 47 +- config/crypto.go | 40 +- config/discovery.go | 17 - config/email.go | 34 - config/jwt.go | 6 +- config/linqu.go | 49 - config/log.go | 6 + config/logger.go | 9 - config/midtrans.go | 25 - config/order.go | 12 - config/{oss.go => s3.go} | 16 +- config/tuya.go | 1 - config/withdraw.go | 9 - docker-build.sh | 199 ++ docker-compose.yaml | 130 +- docs/ADVANCED_ORDER_MANAGEMENT.md | 463 --- docs/IMPLEMENTATION_SUMMARY.md | 297 -- docs/REFUND_API.md | 271 -- docs/docs.go | 3101 ----------------- docs/swagger.json | 3072 ---------------- docs/swagger.yaml | 1833 ---------- go.mod | 40 +- go.sum | 84 +- infra/development.yaml | 34 + infra/enaklopos.development.yaml | 96 - internal/README.md | 242 ++ internal/app/app.go | 284 ++ internal/app/server.go | 34 +- internal/appcontext/context.go | 80 + internal/appcontext/context_info.go | 81 + .../oss/oss.go => client/s3_flie_client.go} | 22 +- internal/common/.DS_Store | Bin 6148 -> 0 bytes internal/common/database/database.go | 5 - internal/common/errors/code.go | 52 - internal/common/errors/errors.go | 142 - internal/common/http/http.go | 46 - internal/common/logger/logger.go | 56 - internal/common/mycontext/kinoscontext.go | 96 - internal/common/request/context.go | 53 - internal/common/request/request.go | 40 - internal/constants/.DS_Store | Bin 6148 -> 0 bytes internal/constants/branch/branch.go | 12 - internal/constants/business.go | 63 + internal/constants/constant.go | 17 + internal/constants/constants.go | 66 - internal/constants/device/device.go | 12 - .../constants/device/device_connection.go | 12 - internal/constants/error.go | 57 + internal/constants/file.go | 80 + internal/constants/header.go | 27 + internal/constants/order.go | 87 + internal/constants/order/order.go | 87 - internal/constants/organization.go | 26 + internal/constants/oss.go | 9 - internal/constants/outlet.go | 27 + internal/constants/payment.go | 82 + internal/constants/product/product.go | 58 - internal/constants/role/role.go | 12 - internal/constants/studio/studio.go | 12 - internal/constants/transaction/transaction.go | 29 - internal/constants/user.go | 28 + internal/constants/userstatus/userstatus.go | 12 - internal/contract/analytics_contract.go | 210 ++ internal/contract/category_contract.go | 49 + internal/contract/common.go | 49 + internal/contract/customer_contract.go | 58 + internal/contract/file_contract.go | 73 + internal/contract/inventory_contract.go | 72 + internal/contract/order_contract.go | 215 ++ internal/contract/organization_contract.go | 61 + internal/contract/outlet_contract.go | 57 + internal/contract/payment_method_contract.go | 53 + internal/contract/product_contract.go | 103 + internal/contract/response.go | 39 + internal/contract/response_error.go | 33 + internal/contract/user_contract.go | 69 + internal/{common => }/db/database.go | 13 +- internal/entities/analytics.go | 102 + internal/entities/category.go | 56 + internal/entities/customer.go | 36 + internal/entities/entities.go | 26 + internal/entities/file.go | 28 + internal/entities/inventory.go | 42 + internal/entities/order.go | 110 + internal/entities/order_item.go | 89 + internal/entities/order_sequence.go | 33 + internal/entities/organization.go | 34 + internal/entities/outlet.go | 38 + internal/entities/outlet_setting.go | 30 + internal/entities/payment.go | 93 + internal/entities/payment_order_item.go | 31 + internal/entities/product.go | 66 + internal/entities/user.go | 94 + internal/entity/auth.go | 219 -- internal/entity/balance.go | 25 - internal/entity/casheer_session.go | 28 - internal/entity/category.go | 9 - internal/entity/cust.go | 18 - internal/entity/customer.go | 2 - internal/entity/discovery.go | 43 - internal/entity/email.go | 13 - internal/entity/event.go | 158 - internal/entity/in_progress_order.go | 29 - internal/entity/jwt.go | 38 - internal/entity/license.go | 162 - internal/entity/linkqu.go | 98 - internal/entity/member.go | 83 - internal/entity/midtrans.go | 19 - internal/entity/order.go | 308 -- internal/entity/order_inquiry.go | 135 - internal/entity/oss.go | 24 - internal/entity/partner.go | 253 -- internal/entity/partner_setting.go | 56 - internal/entity/payment.go | 26 - internal/entity/payment_gateway.go | 23 - internal/entity/product.go | 177 - internal/entity/search.go | 32 - internal/entity/sites.go | 210 -- internal/entity/studio.go | 95 - internal/entity/transaction.go | 62 - internal/entity/undian.go | 118 - internal/entity/user.go | 143 - internal/entity/wallet.go | 27 - internal/handler/analytics_handler.go | 157 + internal/handler/auth_handler.go | 169 + internal/handler/category_handler.go | 189 + internal/handler/common.go | 57 + internal/handler/customer_handler.go | 257 ++ internal/handler/file_handler.go | 181 + internal/handler/health.go | 23 + internal/handler/inventory_handler.go | 278 ++ internal/handler/order_handler.go | 296 ++ internal/handler/organization_handler.go | 185 + internal/handler/outlet_handler.go | 144 + internal/handler/outlet_setting_handler.go | 226 ++ internal/handler/payment_method_handler.go | 216 ++ internal/handler/product_handler.go | 213 ++ internal/handler/product_variant_handler.go | 156 + internal/handler/user_handler.go | 319 ++ internal/handler/user_service.go | 19 + internal/handler/user_validator.go | 14 + internal/handlers/.DS_Store | Bin 6148 -> 0 bytes internal/handlers/http/.DS_Store | Bin 6148 -> 0 bytes internal/handlers/http/auth/auth.go | 175 - internal/handlers/http/balance/balance.go | 129 - internal/handlers/http/cashier.go | 169 - internal/handlers/http/categories.go | 139 - internal/handlers/http/customer.go | 81 - internal/handlers/http/customer_order.go | 168 - internal/handlers/http/customer_undian.go | 164 - internal/handlers/http/customerauth/auth.go | 137 - internal/handlers/http/inprogress_order.go | 224 -- internal/handlers/http/license/license.go | 160 - internal/handlers/http/linqu/order.go | 64 - internal/handlers/http/member.go | 154 - internal/handlers/http/menu.go | 213 -- internal/handlers/http/midtrans/order.go | 71 - internal/handlers/http/order.go | 855 ----- internal/handlers/http/oss/oss.go | 92 - internal/handlers/http/partner/partner.go | 295 -- internal/handlers/http/product/product.go | 303 -- internal/handlers/http/sites/sites.go | 324 -- .../handlers/http/transaction/transaction.go | 119 - internal/handlers/http/user/user.go | 533 --- internal/handlers/request/auth.go | 78 - internal/handlers/request/balance.go | 23 - internal/handlers/request/cashier.go | 20 - internal/handlers/request/category.go | 14 - internal/handlers/request/context.go | 20 - internal/handlers/request/customer.go | 49 - internal/handlers/request/discovery.go | 37 - internal/handlers/request/event.go | 86 - internal/handlers/request/license.go | 52 - internal/handlers/request/member.go | 36 - internal/handlers/request/midtrans.go | 58 - internal/handlers/request/order.go | 178 - internal/handlers/request/partner.go | 99 - internal/handlers/request/product.go | 57 - internal/handlers/request/query.go | 126 - internal/handlers/request/site.go | 90 - internal/handlers/request/studio.go | 53 - internal/handlers/request/transaction.go | 64 - internal/handlers/request/user.go | 178 - internal/handlers/request/validator.go | 50 - .../request/validator/request_validator.go | 43 - internal/handlers/response/auth.go | 28 - internal/handlers/response/base_response.go | 13 - internal/handlers/response/branch.go | 20 - internal/handlers/response/cashier.go | 64 - internal/handlers/response/category.go | 29 - internal/handlers/response/customer.go | 41 - internal/handlers/response/discovery.go | 83 - internal/handlers/response/event.go | 27 - internal/handlers/response/handler.go | 43 - internal/handlers/response/license.go | 72 - internal/handlers/response/member.go | 111 - internal/handlers/response/order.go | 256 -- internal/handlers/response/order_inquiry.go | 133 - .../handlers/response/pagination_formatter.go | 34 - internal/handlers/response/paging.gp.go | 7 - internal/handlers/response/partner.go | 25 - .../handlers/response/payment_formatter.go | 20 - internal/handlers/response/product.go | 24 - internal/handlers/response/site.go | 41 - internal/handlers/response/studio.go | 19 - internal/handlers/response/transaction.go | 20 - internal/handlers/response/undian.go | 79 - internal/handlers/response/user.go | 55 - internal/logger/app_logger.go | 134 + internal/mappers/category_mapper.go | 154 + internal/mappers/customer_mapper.go | 71 + internal/mappers/file_mapper.go | 98 + internal/mappers/inventory_mapper.go | 114 + internal/mappers/order_mapper.go | 298 ++ internal/mappers/order_mapper_test.go | 142 + internal/mappers/organization_mapper.go | 79 + internal/mappers/outlet_mapper.go | 37 + internal/mappers/payment_method_mapper.go | 179 + internal/mappers/product_mapper.go | 315 ++ internal/mappers/user_mapper.go | 110 + internal/middleware/auth_middleware.go | 141 + internal/middleware/auth_processor.go | 4 + internal/middleware/context.go | 67 + internal/middleware/correlation_id.go | 22 + internal/middleware/cors.go | 21 + internal/middleware/json.go | 17 + internal/middleware/logging.go | 24 + internal/middleware/rate_limit.go | 69 + internal/middleware/recover.go | 30 + internal/middleware/stat_logger.go | 35 + internal/middleware/user_id_resolver.go | 64 + internal/middleware/user_processor.go | 11 + internal/middlewares/auth.go | 141 - internal/middlewares/cors.go | 38 - internal/middlewares/logger.go | 43 - internal/middlewares/request.go | 136 - internal/middlewares/trace.go | 20 - internal/models/analytics.go | 214 ++ internal/models/category.go | 47 + internal/models/customer.go | 53 + internal/models/file.go | 102 + internal/models/inventory.go | 59 + internal/models/models.go | 32 + internal/models/order.go | 288 ++ internal/models/organization.go | 76 + internal/models/outlet.go | 41 + internal/models/outlet_setting.go | 53 + internal/models/payment.go | 81 + internal/models/payment_method.go | 77 + internal/models/product.go | 126 + internal/models/user.go | 88 + internal/processor/analytics_processor.go | 347 ++ internal/processor/category_processor.go | 152 + internal/processor/customer_processor.go | 195 ++ internal/processor/file_manager_client.go | 7 + internal/processor/file_manager_processor.go | 227 ++ internal/processor/inventory_processor.go | 245 ++ internal/processor/order_processor.go | 884 +++++ internal/processor/organization_processor.go | 186 + internal/processor/organization_repository.go | 20 + internal/processor/outlet_processor.go | 149 + internal/processor/outlet_repository.go | 20 + .../processor/outlet_setting_processor.go | 232 ++ .../processor/payment_method_processor.go | 190 + internal/processor/product_processor.go | 307 ++ internal/processor/product_processor_test.go | 180 + .../processor/product_variant_processor.go | 138 + internal/processor/user_processor.go | 226 ++ internal/processor/user_repository.go | 22 + internal/repository/.DS_Store | Bin 6148 -> 0 bytes internal/repository/In_progress_orde_repo.go | 337 -- internal/repository/analytics_repository.go | 328 ++ internal/repository/auth/exec.go | 1 - internal/repository/auth/init.go | 98 - internal/repository/brevo/init.go | 109 - internal/repository/casheer_seasion.go | 177 - internal/repository/categories_repo.go | 94 - internal/repository/category_repository.go | 125 + internal/repository/crypto/crypto.go | 4 - internal/repository/crypto/init.go | 324 -- internal/repository/customer_repo.go | 387 -- internal/repository/customer_repository.go | 140 + internal/repository/file_repository.go | 109 + internal/repository/inventory_repository.go | 301 ++ internal/repository/license/license.go | 122 - internal/repository/linkqu/linkqu.go | 304 -- internal/repository/member_repo.go | 140 - internal/repository/midtrans/init.go | 126 - internal/repository/models/casheer_seasion.go | 21 - internal/repository/models/categories.go | 16 - internal/repository/models/customer.go | 78 - .../repository/models/in_progress_order.go | 35 - internal/repository/models/member.go | 26 - internal/repository/models/order.go | 96 - internal/repository/models/partner_setting.go | 53 - internal/repository/models/product.go | 23 - internal/repository/models/transaction.go | 21 - internal/repository/orde_repo.go | 1073 ------ internal/repository/order_item_repository.go | 136 + internal/repository/order_repository.go | 237 ++ internal/repository/orders/order.go | 354 -- .../repository/organization_repository.go | 101 + internal/repository/outlet_repository.go | 105 + .../repository/outlet_setting_repository.go | 87 + internal/repository/partner_settings.go | 225 -- internal/repository/partners/partners.go | 143 - internal/repository/payment/payment.go | 84 - .../payment_gateway/paymentgateway.go | 136 - .../repository/payment_method_repository.go | 119 + internal/repository/payment_repository.go | 94 + internal/repository/product_repo.go | 118 - internal/repository/product_repository.go | 191 + .../repository/product_variant_repository.go | 78 + internal/repository/products/product.go | 132 - internal/repository/query_builder.go | 193 - internal/repository/repository.go | 245 -- internal/repository/sites/sites.go | 282 -- internal/repository/transaction.go | 32 - .../repository/transaction/transaction.go | 179 - internal/repository/transaction_repo.go | 77 - internal/repository/trx/trx.go | 33 - internal/repository/undian_repo.go | 140 - internal/repository/user_repository.go | 112 + internal/repository/users/user.go | 275 -- internal/repository/wallet/wallet.go | 94 - internal/router/router.go | 262 ++ internal/routes/customer_routes.go | 31 - internal/routes/routes.go | 81 - internal/service/analytics_service.go | 243 ++ internal/service/auth_service.go | 184 + internal/service/category_service.go | 119 + internal/service/customer_service.go | 111 + internal/service/file_service.go | 166 + internal/service/inventory_service.go | 166 + internal/service/order_service.go | 386 ++ internal/service/organization_service.go | 119 + internal/service/outlet_service.go | 106 + internal/service/outlet_setting_service.go | 52 + internal/service/payment_method_service.go | 130 + internal/service/product_service.go | 131 + internal/service/product_variant_service.go | 91 + internal/service/user_processor.go | 22 + internal/service/user_service.go | 101 + internal/services/.DS_Store | Bin 6148 -> 0 bytes internal/services/auth/init.go | 202 -- internal/services/balance/balance.go | 138 - internal/services/license/license.go | 71 - internal/services/member/member.go | 58 - .../services/member/member_registration.go | 270 -- internal/services/oss/impl.go | 45 - internal/services/oss/init.go | 13 - internal/services/partner/partner.go | 167 - internal/services/product/product.go | 99 - internal/services/service.go | 154 - internal/services/sites/sites.go | 112 - internal/services/transaction/transaction.go | 111 - internal/services/users/users.go | 183 - internal/services/v2/auth/auth.go | 58 - .../v2/cashier_session/casheer_session.go | 110 - internal/services/v2/categories/categories.go | 88 - internal/services/v2/customer/customer.go | 360 -- .../v2/inprogress_order/in_progress_order.go | 321 -- .../in_progress_order_test.go | 898 ----- internal/services/v2/member/member.go | 1 - .../v2/order/advanced_order_management.go | 481 --- .../services/v2/order/create_order_inquiry.go | 248 -- internal/services/v2/order/execute_order.go | 321 -- internal/services/v2/order/order.go | 165 - internal/services/v2/order/order_history.go | 55 - .../v2/partner_settings/partner_setting.go | 149 - .../services/v2/product/get_product_by_id.go | 43 - .../v2/product/get_product_details.go | 56 - internal/services/v2/product/product.go | 28 - internal/services/v2/undian/undian.go | 123 - internal/transformer/analytics_transformer.go | 377 ++ internal/transformer/category_transformer.go | 55 + internal/transformer/common_transformer.go | 109 + internal/transformer/customer_transformer.go | 82 + internal/transformer/file_transformer.go | 142 + internal/transformer/inventory_transformer.go | 74 + internal/transformer/order_transformer.go | 353 ++ .../transformer/organization_transformer.go | 91 + internal/transformer/outlet_transformer.go | 45 + internal/transformer/product_transformer.go | 187 + internal/transformer/transformer.go | 38 + internal/transformer/user_transformer.go | 105 + internal/util/http_util.go | 49 + internal/utils/.DS_Store | Bin 6148 -> 0 bytes internal/utils/arrays.go | 20 - internal/utils/bank_code.go | 16 - internal/utils/currency.go | 43 - internal/utils/currency_test.go | 36 - internal/utils/format_validator_error.go | 38 - internal/utils/generator/string-generator.go | 42 - internal/utils/member_generator.go | 14 - internal/validator/category_validator.go | 122 + internal/validator/customer_validator.go | 176 + internal/validator/file_validator.go | 90 + internal/validator/inventory_validator.go | 113 + internal/validator/order_validator.go | 74 + internal/validator/organization_validator.go | 165 + internal/validator/outlet_validator.go | 95 + .../validator/payment_method_validator.go | 108 + internal/validator/product_validator.go | 126 + .../validator/product_variant_validator.go | 64 + internal/validator/user_validator.go | 151 + internal/validator/validator_helpers.go | 32 + k8s/.DS_Store | Bin 6148 -> 0 bytes k8s/production/deployment.yaml | 40 - k8s/production/ingress-cors.yaml | 30 - k8s/production/ingress.yaml | 25 - k8s/production/namespace.yaml | 4 - k8s/production/service.yaml | 15 - k8s/staging/deployment.yaml | 37 - k8s/staging/ingress-cors.yaml | 30 - k8s/staging/ingress.yaml | 25 - k8s/staging/namespace.yaml | 4 - k8s/staging/service.yaml | 15 - main.go | 41 - migrations/000001_add_table_user.down.sql | 1 - migrations/000001_add_table_user.up.sql | 22 - ...000001_create_organizations_table.down.sql | 1 + .../000001_create_organizations_table.up.sql | 14 + migrations/000002_add-role-table.down.sql | 1 - migrations/000002_add-role-table.up.sql | 11 - .../000002_create_outlets_table.down.sql | 1 + migrations/000002_create_outlets_table.up.sql | 18 + migrations/000003_create_users_table.down.sql | 1 + migrations/000003_create_users_table.up.sql | 22 + migrations/000003_user-role-table.down.sql | 1 - migrations/000003_user-role-table.up.sql | 12 - .../000004_create_categories_table.down.sql | 1 + .../000004_create_categories_table.up.sql | 16 + migrations/000004_partner-table.down.sql | 1 - migrations/000004_partner-table.up.sql | 17 - migrations/000005_add_table_sites.down.sql | 1 - migrations/000005_add_table_sites.up.sql | 22 - .../000005_create_products_table.down.sql | 1 + .../000005_create_products_table.up.sql | 24 + migrations/000006_add_table_products.down.sql | 1 - migrations/000006_add_table_products.up.sql | 16 - ...006_create_product_variants_table.down.sql | 1 + ...00006_create_product_variants_table.up.sql | 14 + migrations/000007_add-table-order.down.sql | 1 - migrations/000007_add-table-order.up.sql | 13 - .../000007_create_inventory_table.down.sql | 1 + .../000007_create_inventory_table.up.sql | 16 + .../000008_add-table-order-items.down.sql | 1 - .../000008_add-table-order-items.up.sql | 13 - .../000008_create_orders_table.down.sql | 1 + migrations/000008_create_orders_table.up.sql | 30 + migrations/000009_add_wallet.down.sql | 1 - migrations/000009_add_wallet.up.sql | 11 - .../000009_create_order_items_table.down.sql | 1 + .../000009_create_order_items_table.up.sql | 21 + migrations/000010_add-payment-table.down.sql | 1 - migrations/000010_add-payment-table.up.sql | 16 - ...0010_create_payment_methods_table.down.sql | 1 + ...000010_create_payment_methods_table.up.sql | 18 + migrations/000011_add-license-table.down.sql | 1 - migrations/000011_add-license-table.up.sql | 14 - .../000011_create_payments_table.down.sql | 1 + .../000011_create_payments_table.up.sql | 19 + migrations/000012_add-transaction.down.sql | 1 - migrations/000012_add-transaction.up.sql | 13 - ..._add_email_phone_to_organizations.down.sql | 7 + ...12_add_email_phone_to_organizations.up.sql | 7 + ...0013_add_cost_to_product_variants.down.sql | 2 + ...000013_add_cost_to_product_variants.up.sql | 2 + ...dd_partner_id_to_cashier_sessions.down.sql | 5 - ..._add_partner_id_to_cashier_sessions.up.sql | 8 - ...dd_cost_to_orders_and_order_items.down.sql | 30 + ..._add_cost_to_orders_and_order_items.up.sql | 44 + ...5_add_payment_split_functionality.down.sql | 22 + ...015_add_payment_split_functionality.up.sql | 37 + migrations/000016_create_files_table.down.sql | 12 + migrations/000016_create_files_table.up.sql | 29 + .../000017_create_customers_table.down.sql | 1 + .../000017_create_customers_table.up.sql | 27 + ...0018_add_default_customer_trigger.down.sql | 5 + ...000018_add_default_customer_trigger.up.sql | 15 + ...fault_customers_for_existing_orgs.down.sql | 3 + ...default_customers_for_existing_orgs.up.sql | 14 + ...notes_and_metadata_to_order_items.down.sql | 4 + ...d_notes_and_metadata_to_order_items.up.sql | 4 + .../000021_add_paid_status_to_orders.down.sql | 3 + .../000021_add_paid_status_to_orders.up.sql | 3 + ...0022_create_outlet_settings_table.down.sql | 1 + ...000022_create_outlet_settings_table.up.sql | 15 + server | Bin 0 -> 36710946 bytes templates/member_registration_otp.html | 216 -- templates/monthly_points.html | 154 - templates/reset_password.html | 137 - templates/reset_password_customer.html | 137 - templates/transaction_receipt.html | 194 -- templates/welcome_member.html | 331 -- 511 files changed, 24132 insertions(+), 34137 deletions(-) create mode 100644 .air.toml create mode 100644 ANALYTICS_API.md create mode 100644 DOCKER.md delete mode 100644 Dockerfile copy create mode 100644 ORDER_VOID_STATUS_IMPROVEMENT.md create mode 100644 OUTLET_TAX_CALCULATION.md create mode 100644 PRODUCT_STOCK_MANAGEMENT.md create mode 100644 PRODUCT_VARIANT_PRICE_MODIFIER.md create mode 100644 PROFIT_LOSS_ANALYTICS_API.md create mode 100644 cmd/server/main.go delete mode 100644 config/brevo.go delete mode 100644 config/discovery.go delete mode 100644 config/email.go delete mode 100644 config/linqu.go create mode 100644 config/log.go delete mode 100644 config/logger.go delete mode 100644 config/midtrans.go delete mode 100644 config/order.go rename config/{oss.go => s3.go} (62%) delete mode 100644 config/tuya.go delete mode 100644 config/withdraw.go create mode 100755 docker-build.sh delete mode 100644 docs/ADVANCED_ORDER_MANAGEMENT.md delete mode 100644 docs/IMPLEMENTATION_SUMMARY.md delete mode 100644 docs/REFUND_API.md delete mode 100644 docs/docs.go delete mode 100644 docs/swagger.json delete mode 100644 docs/swagger.yaml create mode 100644 infra/development.yaml delete mode 100644 infra/enaklopos.development.yaml create mode 100644 internal/README.md create mode 100644 internal/app/app.go create mode 100644 internal/appcontext/context.go create mode 100644 internal/appcontext/context_info.go rename internal/{repository/oss/oss.go => client/s3_flie_client.go} (64%) delete mode 100644 internal/common/.DS_Store delete mode 100644 internal/common/database/database.go delete mode 100644 internal/common/errors/code.go delete mode 100644 internal/common/errors/errors.go delete mode 100644 internal/common/http/http.go delete mode 100644 internal/common/logger/logger.go delete mode 100644 internal/common/mycontext/kinoscontext.go delete mode 100644 internal/common/request/context.go delete mode 100644 internal/common/request/request.go delete mode 100644 internal/constants/.DS_Store delete mode 100644 internal/constants/branch/branch.go create mode 100644 internal/constants/business.go create mode 100644 internal/constants/constant.go delete mode 100644 internal/constants/constants.go delete mode 100644 internal/constants/device/device.go delete mode 100644 internal/constants/device/device_connection.go create mode 100644 internal/constants/error.go create mode 100644 internal/constants/file.go create mode 100644 internal/constants/header.go create mode 100644 internal/constants/order.go delete mode 100644 internal/constants/order/order.go create mode 100644 internal/constants/organization.go delete mode 100644 internal/constants/oss.go create mode 100644 internal/constants/outlet.go create mode 100644 internal/constants/payment.go delete mode 100644 internal/constants/product/product.go delete mode 100644 internal/constants/role/role.go delete mode 100644 internal/constants/studio/studio.go delete mode 100644 internal/constants/transaction/transaction.go create mode 100644 internal/constants/user.go delete mode 100644 internal/constants/userstatus/userstatus.go create mode 100644 internal/contract/analytics_contract.go create mode 100644 internal/contract/category_contract.go create mode 100644 internal/contract/common.go create mode 100644 internal/contract/customer_contract.go create mode 100644 internal/contract/file_contract.go create mode 100644 internal/contract/inventory_contract.go create mode 100644 internal/contract/order_contract.go create mode 100644 internal/contract/organization_contract.go create mode 100644 internal/contract/outlet_contract.go create mode 100644 internal/contract/payment_method_contract.go create mode 100644 internal/contract/product_contract.go create mode 100644 internal/contract/response.go create mode 100644 internal/contract/response_error.go create mode 100644 internal/contract/user_contract.go rename internal/{common => }/db/database.go (68%) create mode 100644 internal/entities/analytics.go create mode 100644 internal/entities/category.go create mode 100644 internal/entities/customer.go create mode 100644 internal/entities/entities.go create mode 100644 internal/entities/file.go create mode 100644 internal/entities/inventory.go create mode 100644 internal/entities/order.go create mode 100644 internal/entities/order_item.go create mode 100644 internal/entities/order_sequence.go create mode 100644 internal/entities/organization.go create mode 100644 internal/entities/outlet.go create mode 100644 internal/entities/outlet_setting.go create mode 100644 internal/entities/payment.go create mode 100644 internal/entities/payment_order_item.go create mode 100644 internal/entities/product.go create mode 100644 internal/entities/user.go delete mode 100644 internal/entity/auth.go delete mode 100644 internal/entity/balance.go delete mode 100644 internal/entity/casheer_session.go delete mode 100644 internal/entity/category.go delete mode 100644 internal/entity/cust.go delete mode 100644 internal/entity/customer.go delete mode 100644 internal/entity/discovery.go delete mode 100644 internal/entity/email.go delete mode 100644 internal/entity/event.go delete mode 100644 internal/entity/in_progress_order.go delete mode 100644 internal/entity/jwt.go delete mode 100644 internal/entity/license.go delete mode 100644 internal/entity/linkqu.go delete mode 100644 internal/entity/member.go delete mode 100644 internal/entity/midtrans.go delete mode 100644 internal/entity/order.go delete mode 100644 internal/entity/order_inquiry.go delete mode 100644 internal/entity/oss.go delete mode 100644 internal/entity/partner.go delete mode 100644 internal/entity/partner_setting.go delete mode 100644 internal/entity/payment.go delete mode 100644 internal/entity/payment_gateway.go delete mode 100644 internal/entity/product.go delete mode 100644 internal/entity/search.go delete mode 100644 internal/entity/sites.go delete mode 100644 internal/entity/studio.go delete mode 100644 internal/entity/transaction.go delete mode 100644 internal/entity/undian.go delete mode 100644 internal/entity/user.go delete mode 100644 internal/entity/wallet.go create mode 100644 internal/handler/analytics_handler.go create mode 100644 internal/handler/auth_handler.go create mode 100644 internal/handler/category_handler.go create mode 100644 internal/handler/common.go create mode 100644 internal/handler/customer_handler.go create mode 100644 internal/handler/file_handler.go create mode 100644 internal/handler/health.go create mode 100644 internal/handler/inventory_handler.go create mode 100644 internal/handler/order_handler.go create mode 100644 internal/handler/organization_handler.go create mode 100644 internal/handler/outlet_handler.go create mode 100644 internal/handler/outlet_setting_handler.go create mode 100644 internal/handler/payment_method_handler.go create mode 100644 internal/handler/product_handler.go create mode 100644 internal/handler/product_variant_handler.go create mode 100644 internal/handler/user_handler.go create mode 100644 internal/handler/user_service.go create mode 100644 internal/handler/user_validator.go delete mode 100644 internal/handlers/.DS_Store delete mode 100644 internal/handlers/http/.DS_Store delete mode 100644 internal/handlers/http/auth/auth.go delete mode 100644 internal/handlers/http/balance/balance.go delete mode 100644 internal/handlers/http/cashier.go delete mode 100644 internal/handlers/http/categories.go delete mode 100644 internal/handlers/http/customer.go delete mode 100644 internal/handlers/http/customer_order.go delete mode 100644 internal/handlers/http/customer_undian.go delete mode 100644 internal/handlers/http/customerauth/auth.go delete mode 100644 internal/handlers/http/inprogress_order.go delete mode 100644 internal/handlers/http/license/license.go delete mode 100644 internal/handlers/http/linqu/order.go delete mode 100644 internal/handlers/http/member.go delete mode 100644 internal/handlers/http/menu.go delete mode 100644 internal/handlers/http/midtrans/order.go delete mode 100644 internal/handlers/http/order.go delete mode 100644 internal/handlers/http/oss/oss.go delete mode 100644 internal/handlers/http/partner/partner.go delete mode 100644 internal/handlers/http/product/product.go delete mode 100644 internal/handlers/http/sites/sites.go delete mode 100644 internal/handlers/http/transaction/transaction.go delete mode 100644 internal/handlers/http/user/user.go delete mode 100644 internal/handlers/request/auth.go delete mode 100644 internal/handlers/request/balance.go delete mode 100644 internal/handlers/request/cashier.go delete mode 100644 internal/handlers/request/category.go delete mode 100644 internal/handlers/request/context.go delete mode 100644 internal/handlers/request/customer.go delete mode 100644 internal/handlers/request/discovery.go delete mode 100644 internal/handlers/request/event.go delete mode 100644 internal/handlers/request/license.go delete mode 100644 internal/handlers/request/member.go delete mode 100644 internal/handlers/request/midtrans.go delete mode 100644 internal/handlers/request/order.go delete mode 100644 internal/handlers/request/partner.go delete mode 100644 internal/handlers/request/product.go delete mode 100644 internal/handlers/request/query.go delete mode 100644 internal/handlers/request/site.go delete mode 100644 internal/handlers/request/studio.go delete mode 100644 internal/handlers/request/transaction.go delete mode 100644 internal/handlers/request/user.go delete mode 100644 internal/handlers/request/validator.go delete mode 100644 internal/handlers/request/validator/request_validator.go delete mode 100644 internal/handlers/response/auth.go delete mode 100644 internal/handlers/response/base_response.go delete mode 100644 internal/handlers/response/branch.go delete mode 100644 internal/handlers/response/cashier.go delete mode 100644 internal/handlers/response/category.go delete mode 100644 internal/handlers/response/customer.go delete mode 100644 internal/handlers/response/discovery.go delete mode 100644 internal/handlers/response/event.go delete mode 100644 internal/handlers/response/handler.go delete mode 100644 internal/handlers/response/license.go delete mode 100644 internal/handlers/response/member.go delete mode 100644 internal/handlers/response/order.go delete mode 100644 internal/handlers/response/order_inquiry.go delete mode 100644 internal/handlers/response/pagination_formatter.go delete mode 100644 internal/handlers/response/paging.gp.go delete mode 100644 internal/handlers/response/partner.go delete mode 100644 internal/handlers/response/payment_formatter.go delete mode 100644 internal/handlers/response/product.go delete mode 100644 internal/handlers/response/site.go delete mode 100644 internal/handlers/response/studio.go delete mode 100644 internal/handlers/response/transaction.go delete mode 100644 internal/handlers/response/undian.go delete mode 100644 internal/handlers/response/user.go create mode 100644 internal/logger/app_logger.go create mode 100644 internal/mappers/category_mapper.go create mode 100644 internal/mappers/customer_mapper.go create mode 100644 internal/mappers/file_mapper.go create mode 100644 internal/mappers/inventory_mapper.go create mode 100644 internal/mappers/order_mapper.go create mode 100644 internal/mappers/order_mapper_test.go create mode 100644 internal/mappers/organization_mapper.go create mode 100644 internal/mappers/outlet_mapper.go create mode 100644 internal/mappers/payment_method_mapper.go create mode 100644 internal/mappers/product_mapper.go create mode 100644 internal/mappers/user_mapper.go create mode 100644 internal/middleware/auth_middleware.go create mode 100644 internal/middleware/auth_processor.go create mode 100644 internal/middleware/context.go create mode 100644 internal/middleware/correlation_id.go create mode 100644 internal/middleware/cors.go create mode 100644 internal/middleware/json.go create mode 100644 internal/middleware/logging.go create mode 100644 internal/middleware/rate_limit.go create mode 100644 internal/middleware/recover.go create mode 100644 internal/middleware/stat_logger.go create mode 100644 internal/middleware/user_id_resolver.go create mode 100644 internal/middleware/user_processor.go delete mode 100644 internal/middlewares/auth.go delete mode 100644 internal/middlewares/cors.go delete mode 100644 internal/middlewares/logger.go delete mode 100644 internal/middlewares/request.go delete mode 100644 internal/middlewares/trace.go create mode 100644 internal/models/analytics.go create mode 100644 internal/models/category.go create mode 100644 internal/models/customer.go create mode 100644 internal/models/file.go create mode 100644 internal/models/inventory.go create mode 100644 internal/models/models.go create mode 100644 internal/models/order.go create mode 100644 internal/models/organization.go create mode 100644 internal/models/outlet.go create mode 100644 internal/models/outlet_setting.go create mode 100644 internal/models/payment.go create mode 100644 internal/models/payment_method.go create mode 100644 internal/models/product.go create mode 100644 internal/models/user.go create mode 100644 internal/processor/analytics_processor.go create mode 100644 internal/processor/category_processor.go create mode 100644 internal/processor/customer_processor.go create mode 100644 internal/processor/file_manager_client.go create mode 100644 internal/processor/file_manager_processor.go create mode 100644 internal/processor/inventory_processor.go create mode 100644 internal/processor/order_processor.go create mode 100644 internal/processor/organization_processor.go create mode 100644 internal/processor/organization_repository.go create mode 100644 internal/processor/outlet_processor.go create mode 100644 internal/processor/outlet_repository.go create mode 100644 internal/processor/outlet_setting_processor.go create mode 100644 internal/processor/payment_method_processor.go create mode 100644 internal/processor/product_processor.go create mode 100644 internal/processor/product_processor_test.go create mode 100644 internal/processor/product_variant_processor.go create mode 100644 internal/processor/user_processor.go create mode 100644 internal/processor/user_repository.go delete mode 100644 internal/repository/.DS_Store delete mode 100644 internal/repository/In_progress_orde_repo.go create mode 100644 internal/repository/analytics_repository.go delete mode 100644 internal/repository/auth/exec.go delete mode 100644 internal/repository/auth/init.go delete mode 100644 internal/repository/brevo/init.go delete mode 100644 internal/repository/casheer_seasion.go delete mode 100644 internal/repository/categories_repo.go create mode 100644 internal/repository/category_repository.go delete mode 100644 internal/repository/crypto/crypto.go delete mode 100644 internal/repository/crypto/init.go delete mode 100644 internal/repository/customer_repo.go create mode 100644 internal/repository/customer_repository.go create mode 100644 internal/repository/file_repository.go create mode 100644 internal/repository/inventory_repository.go delete mode 100644 internal/repository/license/license.go delete mode 100644 internal/repository/linkqu/linkqu.go delete mode 100644 internal/repository/member_repo.go delete mode 100644 internal/repository/midtrans/init.go delete mode 100644 internal/repository/models/casheer_seasion.go delete mode 100644 internal/repository/models/categories.go delete mode 100644 internal/repository/models/customer.go delete mode 100644 internal/repository/models/in_progress_order.go delete mode 100644 internal/repository/models/member.go delete mode 100644 internal/repository/models/order.go delete mode 100644 internal/repository/models/partner_setting.go delete mode 100644 internal/repository/models/product.go delete mode 100644 internal/repository/models/transaction.go delete mode 100644 internal/repository/orde_repo.go create mode 100644 internal/repository/order_item_repository.go create mode 100644 internal/repository/order_repository.go delete mode 100644 internal/repository/orders/order.go create mode 100644 internal/repository/organization_repository.go create mode 100644 internal/repository/outlet_repository.go create mode 100644 internal/repository/outlet_setting_repository.go delete mode 100644 internal/repository/partner_settings.go delete mode 100644 internal/repository/partners/partners.go delete mode 100644 internal/repository/payment/payment.go delete mode 100644 internal/repository/payment_gateway/paymentgateway.go create mode 100644 internal/repository/payment_method_repository.go create mode 100644 internal/repository/payment_repository.go delete mode 100644 internal/repository/product_repo.go create mode 100644 internal/repository/product_repository.go create mode 100644 internal/repository/product_variant_repository.go delete mode 100644 internal/repository/products/product.go delete mode 100644 internal/repository/query_builder.go delete mode 100644 internal/repository/repository.go delete mode 100644 internal/repository/sites/sites.go delete mode 100644 internal/repository/transaction.go delete mode 100644 internal/repository/transaction/transaction.go delete mode 100644 internal/repository/transaction_repo.go delete mode 100644 internal/repository/trx/trx.go delete mode 100644 internal/repository/undian_repo.go create mode 100644 internal/repository/user_repository.go delete mode 100644 internal/repository/users/user.go delete mode 100644 internal/repository/wallet/wallet.go create mode 100644 internal/router/router.go delete mode 100644 internal/routes/customer_routes.go delete mode 100644 internal/routes/routes.go create mode 100644 internal/service/analytics_service.go create mode 100644 internal/service/auth_service.go create mode 100644 internal/service/category_service.go create mode 100644 internal/service/customer_service.go create mode 100644 internal/service/file_service.go create mode 100644 internal/service/inventory_service.go create mode 100644 internal/service/order_service.go create mode 100644 internal/service/organization_service.go create mode 100644 internal/service/outlet_service.go create mode 100644 internal/service/outlet_setting_service.go create mode 100644 internal/service/payment_method_service.go create mode 100644 internal/service/product_service.go create mode 100644 internal/service/product_variant_service.go create mode 100644 internal/service/user_processor.go create mode 100644 internal/service/user_service.go delete mode 100644 internal/services/.DS_Store delete mode 100644 internal/services/auth/init.go delete mode 100644 internal/services/balance/balance.go delete mode 100644 internal/services/license/license.go delete mode 100644 internal/services/member/member.go delete mode 100644 internal/services/member/member_registration.go delete mode 100644 internal/services/oss/impl.go delete mode 100644 internal/services/oss/init.go delete mode 100644 internal/services/partner/partner.go delete mode 100644 internal/services/product/product.go delete mode 100644 internal/services/service.go delete mode 100644 internal/services/sites/sites.go delete mode 100644 internal/services/transaction/transaction.go delete mode 100644 internal/services/users/users.go delete mode 100644 internal/services/v2/auth/auth.go delete mode 100644 internal/services/v2/cashier_session/casheer_session.go delete mode 100644 internal/services/v2/categories/categories.go delete mode 100644 internal/services/v2/customer/customer.go delete mode 100644 internal/services/v2/inprogress_order/in_progress_order.go delete mode 100644 internal/services/v2/inprogress_order/in_progress_order_test.go delete mode 100644 internal/services/v2/member/member.go delete mode 100644 internal/services/v2/order/advanced_order_management.go delete mode 100644 internal/services/v2/order/create_order_inquiry.go delete mode 100644 internal/services/v2/order/execute_order.go delete mode 100644 internal/services/v2/order/order.go delete mode 100644 internal/services/v2/order/order_history.go delete mode 100644 internal/services/v2/partner_settings/partner_setting.go delete mode 100644 internal/services/v2/product/get_product_by_id.go delete mode 100644 internal/services/v2/product/get_product_details.go delete mode 100644 internal/services/v2/product/product.go delete mode 100644 internal/services/v2/undian/undian.go create mode 100644 internal/transformer/analytics_transformer.go create mode 100644 internal/transformer/category_transformer.go create mode 100644 internal/transformer/common_transformer.go create mode 100644 internal/transformer/customer_transformer.go create mode 100644 internal/transformer/file_transformer.go create mode 100644 internal/transformer/inventory_transformer.go create mode 100644 internal/transformer/order_transformer.go create mode 100644 internal/transformer/organization_transformer.go create mode 100644 internal/transformer/outlet_transformer.go create mode 100644 internal/transformer/product_transformer.go create mode 100644 internal/transformer/transformer.go create mode 100644 internal/transformer/user_transformer.go create mode 100644 internal/util/http_util.go delete mode 100644 internal/utils/.DS_Store delete mode 100644 internal/utils/arrays.go delete mode 100644 internal/utils/bank_code.go delete mode 100644 internal/utils/currency.go delete mode 100644 internal/utils/currency_test.go delete mode 100644 internal/utils/format_validator_error.go delete mode 100644 internal/utils/generator/string-generator.go delete mode 100644 internal/utils/member_generator.go create mode 100644 internal/validator/category_validator.go create mode 100644 internal/validator/customer_validator.go create mode 100644 internal/validator/file_validator.go create mode 100644 internal/validator/inventory_validator.go create mode 100644 internal/validator/order_validator.go create mode 100644 internal/validator/organization_validator.go create mode 100644 internal/validator/outlet_validator.go create mode 100644 internal/validator/payment_method_validator.go create mode 100644 internal/validator/product_validator.go create mode 100644 internal/validator/product_variant_validator.go create mode 100644 internal/validator/user_validator.go create mode 100644 internal/validator/validator_helpers.go delete mode 100644 k8s/.DS_Store delete mode 100644 k8s/production/deployment.yaml delete mode 100644 k8s/production/ingress-cors.yaml delete mode 100644 k8s/production/ingress.yaml delete mode 100644 k8s/production/namespace.yaml delete mode 100644 k8s/production/service.yaml delete mode 100644 k8s/staging/deployment.yaml delete mode 100644 k8s/staging/ingress-cors.yaml delete mode 100644 k8s/staging/ingress.yaml delete mode 100644 k8s/staging/namespace.yaml delete mode 100644 k8s/staging/service.yaml delete mode 100644 main.go delete mode 100644 migrations/000001_add_table_user.down.sql delete mode 100644 migrations/000001_add_table_user.up.sql create mode 100644 migrations/000001_create_organizations_table.down.sql create mode 100644 migrations/000001_create_organizations_table.up.sql delete mode 100644 migrations/000002_add-role-table.down.sql delete mode 100644 migrations/000002_add-role-table.up.sql create mode 100644 migrations/000002_create_outlets_table.down.sql create mode 100644 migrations/000002_create_outlets_table.up.sql create mode 100644 migrations/000003_create_users_table.down.sql create mode 100644 migrations/000003_create_users_table.up.sql delete mode 100644 migrations/000003_user-role-table.down.sql delete mode 100644 migrations/000003_user-role-table.up.sql create mode 100644 migrations/000004_create_categories_table.down.sql create mode 100644 migrations/000004_create_categories_table.up.sql delete mode 100644 migrations/000004_partner-table.down.sql delete mode 100644 migrations/000004_partner-table.up.sql delete mode 100644 migrations/000005_add_table_sites.down.sql delete mode 100644 migrations/000005_add_table_sites.up.sql create mode 100644 migrations/000005_create_products_table.down.sql create mode 100644 migrations/000005_create_products_table.up.sql delete mode 100644 migrations/000006_add_table_products.down.sql delete mode 100644 migrations/000006_add_table_products.up.sql create mode 100644 migrations/000006_create_product_variants_table.down.sql create mode 100644 migrations/000006_create_product_variants_table.up.sql delete mode 100644 migrations/000007_add-table-order.down.sql delete mode 100644 migrations/000007_add-table-order.up.sql create mode 100644 migrations/000007_create_inventory_table.down.sql create mode 100644 migrations/000007_create_inventory_table.up.sql delete mode 100644 migrations/000008_add-table-order-items.down.sql delete mode 100644 migrations/000008_add-table-order-items.up.sql create mode 100644 migrations/000008_create_orders_table.down.sql create mode 100644 migrations/000008_create_orders_table.up.sql delete mode 100644 migrations/000009_add_wallet.down.sql delete mode 100644 migrations/000009_add_wallet.up.sql create mode 100644 migrations/000009_create_order_items_table.down.sql create mode 100644 migrations/000009_create_order_items_table.up.sql delete mode 100644 migrations/000010_add-payment-table.down.sql delete mode 100644 migrations/000010_add-payment-table.up.sql create mode 100644 migrations/000010_create_payment_methods_table.down.sql create mode 100644 migrations/000010_create_payment_methods_table.up.sql delete mode 100644 migrations/000011_add-license-table.down.sql delete mode 100644 migrations/000011_add-license-table.up.sql create mode 100644 migrations/000011_create_payments_table.down.sql create mode 100644 migrations/000011_create_payments_table.up.sql delete mode 100644 migrations/000012_add-transaction.down.sql delete mode 100644 migrations/000012_add-transaction.up.sql create mode 100644 migrations/000012_add_email_phone_to_organizations.down.sql create mode 100644 migrations/000012_add_email_phone_to_organizations.up.sql create mode 100644 migrations/000013_add_cost_to_product_variants.down.sql create mode 100644 migrations/000013_add_cost_to_product_variants.up.sql delete mode 100644 migrations/000013_add_partner_id_to_cashier_sessions.down.sql delete mode 100644 migrations/000013_add_partner_id_to_cashier_sessions.up.sql create mode 100644 migrations/000014_add_cost_to_orders_and_order_items.down.sql create mode 100644 migrations/000014_add_cost_to_orders_and_order_items.up.sql create mode 100644 migrations/000015_add_payment_split_functionality.down.sql create mode 100644 migrations/000015_add_payment_split_functionality.up.sql create mode 100644 migrations/000016_create_files_table.down.sql create mode 100644 migrations/000016_create_files_table.up.sql create mode 100644 migrations/000017_create_customers_table.down.sql create mode 100644 migrations/000017_create_customers_table.up.sql create mode 100644 migrations/000018_add_default_customer_trigger.down.sql create mode 100644 migrations/000018_add_default_customer_trigger.up.sql create mode 100644 migrations/000019_create_default_customers_for_existing_orgs.down.sql create mode 100644 migrations/000019_create_default_customers_for_existing_orgs.up.sql create mode 100644 migrations/000020_add_notes_and_metadata_to_order_items.down.sql create mode 100644 migrations/000020_add_notes_and_metadata_to_order_items.up.sql create mode 100644 migrations/000021_add_paid_status_to_orders.down.sql create mode 100644 migrations/000021_add_paid_status_to_orders.up.sql create mode 100644 migrations/000022_create_outlet_settings_table.down.sql create mode 100644 migrations/000022_create_outlet_settings_table.up.sql create mode 100755 server delete mode 100644 templates/member_registration_otp.html delete mode 100644 templates/monthly_points.html delete mode 100644 templates/reset_password.html delete mode 100644 templates/reset_password_customer.html delete mode 100644 templates/transaction_receipt.html delete mode 100644 templates/welcome_member.html diff --git a/.air.toml b/.air.toml new file mode 100644 index 0000000..46912e3 --- /dev/null +++ b/.air.toml @@ -0,0 +1,44 @@ +root = "." +testdata_dir = "testdata" +tmp_dir = "tmp" + +[build] + args_bin = [] + bin = "./tmp/main" + cmd = "go build -o ./tmp/main cmd/server/main.go" + delay = 1000 + exclude_dir = ["assets", "tmp", "vendor", "testdata"] + exclude_file = [] + exclude_regex = ["_test.go"] + exclude_unchanged = false + follow_symlink = false + full_bin = "" + include_dir = [] + include_ext = ["go", "tpl", "tmpl", "html"] + include_file = [] + kill_delay = "0s" + log = "build-errors.log" + poll = false + poll_interval = 0 + rerun = false + rerun_delay = 500 + send_interrupt = false + stop_on_root = false + +[color] + app = "" + build = "yellow" + main = "magenta" + runner = "green" + watcher = "cyan" + +[log] + main_only = false + time = false + +[misc] + clean_on_exit = false + +[screen] + clear_on_rebuild = false + keep_scroll = true \ No newline at end of file diff --git a/.dockerignore b/.dockerignore index b75eaa9..b8a939d 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,17 +1,76 @@ -# Files -.dockerignore -.editorconfig +# Git +.git .gitignore -.env.* -Dockerfile -Makefile -LICENSE -**/*.md -**/*_test.go -*.out +.gitattributes -bin/ -# Folders -.git/ +# Documentation +*.md +docs/ + +# IDE and editor files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Logs +*.log +logs/ + +# Runtime data +pids/ +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage/ +*.cover + +# Node modules (if any frontend assets) +node_modules/ + +# Temporary files +tmp/ +temp/ + +# Build artifacts +server +*.exe +*.test +*.prof + +# Docker files +Dockerfile +.dockerignore + +# CI/CD .github/ -build/ +.gitlab-ci.yml + +# Environment files +.env +.env.local +.env.*.local + +# Test files +*_test.go + +# Migration files (if not needed in container) +migrations/ + +# Development scripts +scripts/dev/ + +# Cache directories +.cache/ diff --git a/ANALYTICS_API.md b/ANALYTICS_API.md new file mode 100644 index 0000000..a59c3ac --- /dev/null +++ b/ANALYTICS_API.md @@ -0,0 +1,292 @@ +# Analytics API Documentation + +This document describes the analytics APIs implemented for the POS system, providing insights into sales, payment methods, products, and overall business performance. + +## Overview + +The analytics APIs provide comprehensive business intelligence for POS operations, including: + +- **Payment Method Analytics**: Track totals for each payment method by date +- **Sales Analytics**: Monitor sales performance over time +- **Product Analytics**: Analyze product performance and revenue +- **Dashboard Analytics**: Overview of key business metrics + +## Authentication + +All analytics endpoints require authentication and admin/manager privileges. Include the JWT token in the Authorization header: + +``` +Authorization: Bearer +``` + +## Base URL + +``` +GET /api/v1/analytics/{endpoint} +``` + +## Endpoints + +### 1. Payment Method Analytics + +**Endpoint:** `GET /api/v1/analytics/payment-methods` + +**Description:** Get payment method totals for a given date range. This is the primary endpoint for tracking payment method performance. + +**Query Parameters:** +- `organization_id` (required): UUID of the organization +- `outlet_id` (optional): UUID of specific outlet to filter by +- `date_from` (required): Start date (ISO 8601 format: YYYY-MM-DD) +- `date_to` (required): End date (ISO 8601 format: YYYY-MM-DD) +- `group_by` (optional): Grouping interval - "day", "hour", "week", "month" (default: "day") + +**Example Request:** +```bash +curl -X GET "http://localhost:8080/api/v1/analytics/payment-methods?organization_id=123e4567-e89b-12d3-a456-426614174000&date_from=2024-01-01&date_to=2024-01-31" \ + -H "Authorization: Bearer " +``` + +**Response:** +```json +{ + "success": true, + "data": { + "organization_id": "123e4567-e89b-12d3-a456-426614174000", + "outlet_id": null, + "date_from": "2024-01-01T00:00:00Z", + "date_to": "2024-01-31T23:59:59Z", + "group_by": "day", + "summary": { + "total_amount": 15000.00, + "total_orders": 150, + "total_payments": 180, + "average_order_value": 100.00 + }, + "data": [ + { + "payment_method_id": "456e7890-e89b-12d3-a456-426614174001", + "payment_method_name": "Cash", + "payment_method_type": "cash", + "total_amount": 8000.00, + "order_count": 80, + "payment_count": 80, + "percentage": 53.33 + }, + { + "payment_method_id": "789e0123-e89b-12d3-a456-426614174002", + "payment_method_name": "Credit Card", + "payment_method_type": "card", + "total_amount": 7000.00, + "order_count": 70, + "payment_count": 100, + "percentage": 46.67 + } + ] + } +} +``` + +### 2. Sales Analytics + +**Endpoint:** `GET /api/v1/analytics/sales` + +**Description:** Get sales performance data over time. + +**Query Parameters:** +- `organization_id` (required): UUID of the organization +- `outlet_id` (optional): UUID of specific outlet to filter by +- `date_from` (required): Start date (ISO 8601 format: YYYY-MM-DD) +- `date_to` (required): End date (ISO 8601 format: YYYY-MM-DD) +- `group_by` (optional): Grouping interval - "day", "hour", "week", "month" (default: "day") + +**Example Request:** +```bash +curl -X GET "http://localhost:8080/api/v1/analytics/sales?organization_id=123e4567-e89b-12d3-a456-426614174000&date_from=2024-01-01&date_to=2024-01-31&group_by=day" \ + -H "Authorization: Bearer " +``` + +**Response:** +```json +{ + "success": true, + "data": { + "organization_id": "123e4567-e89b-12d3-a456-426614174000", + "outlet_id": null, + "date_from": "2024-01-01T00:00:00Z", + "date_to": "2024-01-31T23:59:59Z", + "group_by": "day", + "summary": { + "total_sales": 15000.00, + "total_orders": 150, + "total_items": 450, + "average_order_value": 100.00, + "total_tax": 1500.00, + "total_discount": 500.00, + "net_sales": 13000.00 + }, + "data": [ + { + "date": "2024-01-01T00:00:00Z", + "sales": 500.00, + "orders": 5, + "items": 15, + "tax": 50.00, + "discount": 20.00, + "net_sales": 430.00 + } + ] + } +} +``` + +### 3. Product Analytics + +**Endpoint:** `GET /api/v1/analytics/products` + +**Description:** Get top-performing products by revenue. + +**Query Parameters:** +- `organization_id` (required): UUID of the organization +- `outlet_id` (optional): UUID of specific outlet to filter by +- `date_from` (required): Start date (ISO 8601 format: YYYY-MM-DD) +- `date_to` (required): End date (ISO 8601 format: YYYY-MM-DD) +- `limit` (optional): Number of products to return (1-100, default: 10) + +**Example Request:** +```bash +curl -X GET "http://localhost:8080/api/v1/analytics/products?organization_id=123e4567-e89b-12d3-a456-426614174000&date_from=2024-01-01&date_to=2024-01-31&limit=5" \ + -H "Authorization: Bearer " +``` + +**Response:** +```json +{ + "success": true, + "data": { + "organization_id": "123e4567-e89b-12d3-a456-426614174000", + "outlet_id": null, + "date_from": "2024-01-01T00:00:00Z", + "date_to": "2024-01-31T23:59:59Z", + "data": [ + { + "product_id": "abc123-e89b-12d3-a456-426614174000", + "product_name": "Coffee Latte", + "category_id": "cat123-e89b-12d3-a456-426614174000", + "category_name": "Beverages", + "quantity_sold": 100, + "revenue": 2500.00, + "average_price": 25.00, + "order_count": 80 + } + ] + } +} +``` + +### 4. Dashboard Analytics + +**Endpoint:** `GET /api/v1/analytics/dashboard` + +**Description:** Get comprehensive dashboard overview with key metrics. + +**Query Parameters:** +- `organization_id` (required): UUID of the organization +- `outlet_id` (optional): UUID of specific outlet to filter by +- `date_from` (required): Start date (ISO 8601 format: YYYY-MM-DD) +- `date_to` (required): End date (ISO 8601 format: YYYY-MM-DD) + +**Example Request:** +```bash +curl -X GET "http://localhost:8080/api/v1/analytics/dashboard?organization_id=123e4567-e89b-12d3-a456-426614174000&date_from=2024-01-01&date_to=2024-01-31" \ + -H "Authorization: Bearer " +``` + +**Response:** +```json +{ + "success": true, + "data": { + "organization_id": "123e4567-e89b-12d3-a456-426614174000", + "outlet_id": null, + "date_from": "2024-01-01T00:00:00Z", + "date_to": "2024-01-31T23:59:59Z", + "overview": { + "total_sales": 15000.00, + "total_orders": 150, + "average_order_value": 100.00, + "total_customers": 120, + "voided_orders": 5, + "refunded_orders": 3 + }, + "top_products": [...], + "payment_methods": [...], + "recent_sales": [...] + } +} +``` + +## Error Responses + +All endpoints return consistent error responses: + +```json +{ + "success": false, + "error": "error_type", + "message": "Error description" +} +``` + +Common error types: +- `invalid_request`: Invalid query parameters +- `validation_failed`: Request validation failed +- `internal_error`: Server-side error +- `unauthorized`: Authentication required + +## Date Format + +All date parameters should be in ISO 8601 format: `YYYY-MM-DD` + +Examples: +- `2024-01-01` (January 1, 2024) +- `2024-12-31` (December 31, 2024) + +## Filtering + +- **Organization-level**: All analytics are scoped to a specific organization +- **Outlet-level**: Optional filtering by specific outlet +- **Date range**: Required date range for all analytics queries +- **Time grouping**: Flexible grouping by hour, day, week, or month + +## Performance Considerations + +- Analytics queries are optimized for read performance +- Large date ranges may take longer to process +- Consider using appropriate date ranges for optimal performance +- Results are cached where possible for better response times + +## Use Cases + +### Payment Method Analysis +- Track which payment methods are most popular +- Monitor payment method trends over time +- Identify payment method preferences by outlet +- Calculate payment method percentages for reporting + +### Sales Performance +- Monitor daily/weekly/monthly sales trends +- Track order volumes and average order values +- Analyze tax and discount patterns +- Compare sales performance across outlets + +### Product Performance +- Identify top-selling products +- Analyze product revenue and profitability +- Track product category performance +- Monitor product order frequency + +### Business Intelligence +- Dashboard overview for management +- Key performance indicators (KPIs) +- Trend analysis and forecasting +- Operational insights for decision making \ No newline at end of file diff --git a/DOCKER.md b/DOCKER.md new file mode 100644 index 0000000..633c8a3 --- /dev/null +++ b/DOCKER.md @@ -0,0 +1,312 @@ +# Docker Setup for APSKEL POS Backend + +This document describes how to run the APSKEL POS Backend using Docker and Docker Compose. + +## Prerequisites + +- Docker (version 20.10 or later) +- Docker Compose (version 2.0 or later) +- Git (for cloning the repository) + +## Quick Start + +### 1. Build and Run Production Environment + +```bash +# Build and start all services +./docker-build.sh run + +# Or manually: +docker-compose up -d +``` + +The application will be available at: +- **Backend API**: http://localhost:3300 +- **Database**: localhost:5432 +- **Redis**: localhost:6379 + +### 2. Development Environment + +```bash +# Start development environment with live reload +./docker-build.sh dev + +# Or manually: +docker-compose --profile dev up -d +``` + +Development environment provides: +- **Backend API (Dev)**: http://localhost:3001 (with live reload) +- **Backend API (Prod)**: http://localhost:3300 +- Auto-restart on code changes using Air + +### 3. Database Migrations + +```bash +# Run database migrations +./docker-build.sh migrate + +# Or manually: +docker-compose --profile migrate up migrate +``` + +## Build Script Usage + +The `docker-build.sh` script provides convenient commands: + +```bash +# Build Docker image +./docker-build.sh build + +# Build and run production environment +./docker-build.sh run + +# Start development environment +./docker-build.sh dev + +# Run database migrations +./docker-build.sh migrate + +# Stop all containers +./docker-build.sh stop + +# Clean up containers and images +./docker-build.sh clean + +# Show container logs +./docker-build.sh logs + +# Show help +./docker-build.sh help +``` + +## Services + +### Backend API +- **Port**: 3300 (production), 3001 (development) +- **Health Check**: http://localhost:3300/health +- **Environment**: Configurable via `infra/` directory +- **User**: Runs as non-root user for security + +### PostgreSQL Database +- **Port**: 5432 +- **Database**: apskel_pos +- **Username**: apskel +- **Password**: See docker-compose.yaml +- **Volumes**: Persistent data storage + +### Redis Cache +- **Port**: 6379 +- **Usage**: Caching and session storage +- **Volumes**: Persistent data storage + +## Environment Configuration + +The application uses configuration files from the `infra/` directory: + +- `infra/development.yaml` - Development configuration +- `infra/production.yaml` - Production configuration (create if needed) + +### Configuration Structure + +```yaml +server: + port: 3300 + +postgresql: + host: postgres # Use service name in Docker + port: 5432 + db: apskel_pos + username: apskel + password: 7a8UJbM2GgBWaseh0lnP3O5i1i5nINXk + +jwt: + token: + secret: "your-jwt-secret" + expires-ttl: 1440 + +s3: + access_key_id: "your-s3-key" + access_key_secret: "your-s3-secret" + endpoint: "your-s3-endpoint" + bucket_name: "your-bucket" + +log: + log_level: "info" + log_format: "json" +``` + +## Docker Compose Profiles + +### Default Profile (Production) +```bash +docker-compose up -d +``` +Starts: postgres, redis, backend + +### Development Profile +```bash +docker-compose --profile dev up -d +``` +Starts: postgres, redis, backend, backend-dev + +### Migration Profile +```bash +docker-compose --profile migrate up migrate +``` +Runs: database migrations + +## Health Checks + +All services include health checks: + +- **Backend**: HTTP GET /health +- **PostgreSQL**: pg_isready command +- **Redis**: Redis ping command + +## Logging + +View logs for specific services: + +```bash +# All services +docker-compose logs -f + +# Backend only +docker-compose logs -f backend + +# Database only +docker-compose logs -f postgres + +# Development backend +docker-compose logs -f backend-dev +``` + +## Volumes + +### Persistent Volumes +- `postgres_data`: Database files +- `redis_data`: Redis persistence files +- `go_modules`: Go module cache (development) + +### Bind Mounts +- `./infra:/infra:ro`: Configuration files (read-only) +- `./migrations:/app/migrations:ro`: Database migrations (read-only) +- `.:/app`: Source code (development only) + +## Security + +### Production Security Features +- Non-root user execution +- Read-only configuration mounts +- Minimal base image (Debian slim) +- Health checks for monitoring +- Resource limits (configurable) + +### Network Security +- Internal Docker network isolation +- Only necessary ports exposed +- Service-to-service communication via Docker network + +## Troubleshooting + +### Common Issues + +1. **Port Already in Use** + ```bash + # Check what's using the port + lsof -i :3300 + + # Change ports in docker-compose.yaml if needed + ``` + +2. **Database Connection Failed** + ```bash + # Check if database is running + docker-compose ps postgres + + # Check database logs + docker-compose logs postgres + ``` + +3. **Permission Denied** + ```bash + # Make sure script is executable + chmod +x docker-build.sh + ``` + +4. **Out of Disk Space** + ```bash + # Clean up unused Docker resources + docker system prune -a + + # Remove old images + docker image prune -a + ``` + +### Debug Mode + +Run containers in debug mode: + +```bash +# Start with debug logs +ENV_MODE=development docker-compose up + +# Enter running container +docker-compose exec backend sh + +# Check application logs +docker-compose logs -f backend +``` + +### Performance Tuning + +For production deployment: + +1. **Resource Limits**: Add resource limits to docker-compose.yaml +2. **Environment**: Use production configuration +3. **Logging**: Adjust log levels +4. **Health Checks**: Tune intervals for your needs + +```yaml +services: + backend: + deploy: + resources: + limits: + cpus: '1.0' + memory: 512M + reservations: + cpus: '0.5' + memory: 256M +``` + +## API Testing + +Once the application is running, test the API: + +```bash +# Health check +curl http://localhost:3300/health + +# Analytics endpoint (requires authentication) +curl -H "Authorization: Bearer " \ + -H "Organization-ID: " \ + "http://localhost:3300/api/v1/analytics/profit-loss?date_from=01-12-2023&date_to=31-12-2023" +``` + +## Deployment + +For production deployment: + +1. Update configuration in `infra/production.yaml` +2. Set appropriate environment variables +3. Use production Docker Compose file +4. Configure reverse proxy (nginx, traefik) +5. Set up SSL certificates +6. Configure monitoring and logging + +```bash +# Production deployment +ENV_MODE=production docker-compose -f docker-compose.prod.yaml up -d +``` \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 68a4c6c..ec0495c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,26 +1,99 @@ # Build Stage FROM golang:1.20-alpine AS build -RUN apk --no-cache add tzdata +# Install necessary packages including CA certificates +RUN apk --no-cache add ca-certificates tzdata git curl WORKDIR /src + +# Copy go mod files first for better caching +COPY go.mod go.sum ./ + +# Download dependencies +RUN go mod download + +# Copy source code COPY . . -# RUN CGO_ENABLED=0 GOOS=linux go build -o /app cmd/klinik-core-service -RUN CGO_ENABLED=0 GOOS=linux go build -o /app main.go +# Build the application +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o /app cmd/server/main.go -RUN ls -la / +# Development Stage +FROM golang:1.20-alpine AS development -# Final Stage -FROM gcr.io/distroless/static +# Install air for live reload and other dev tools +RUN go install github.com/cosmtrek/air@latest -WORKDIR / +# Install necessary packages +RUN apk --no-cache add ca-certificates tzdata git curl -COPY --from=build /usr/share/zoneinfo /usr/share/zoneinfo -COPY --from=build /app /app +WORKDIR /app -# RUN ls -la / +# Copy go mod files +COPY go.mod go.sum ./ +RUN go mod download +# Copy source code +COPY . . + +# Set timezone ENV TZ=Asia/Jakarta +# Expose port +EXPOSE 3300 + +# Use air for live reload in development +CMD ["air", "-c", ".air.toml"] + +# Migration Stage +FROM build AS migration + +# Install migration tool +RUN go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest + +WORKDIR /app + +# Copy migration files +COPY migrations ./migrations +COPY infra ./infra + +# Set the entrypoint for migrations +ENTRYPOINT ["migrate"] + +# Production Stage +FROM debian:bullseye-slim AS production + +# Install minimal runtime dependencies +RUN apt-get update && apt-get install -y \ + ca-certificates \ + tzdata \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# Create non-root user for security +RUN groupadd -r appuser && useradd -r -g appuser appuser + +# Copy the binary +COPY --from=build /app /app + +# Copy configuration files +COPY --from=build /src/infra /infra + +# Change ownership to non-root user +RUN chown -R appuser:appuser /app /infra + +# Set timezone +ENV TZ=Asia/Jakarta + +# Expose port +EXPOSE 3300 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \ + CMD curl -f http://localhost:3300/health || exit 1 + +# Switch to non-root user +USER appuser + +# Set the entrypoint ENTRYPOINT ["/app"] diff --git a/Dockerfile copy b/Dockerfile copy deleted file mode 100644 index 07629bb..0000000 --- a/Dockerfile copy +++ /dev/null @@ -1,24 +0,0 @@ - FROM golang:1.20-alpine AS build - -RUN apk --no-cache add tzdata - -WORKDIR /src -COPY . . - -# RUN CGO_ENABLED=0 GOOS=linux go build -o /app cmd/klinik-core-service -RUN CGO_ENABLED=0 GOOS=linux go build -o /app main.go - -RUN ls -la / - -FROM gcr.io/distroless/static - -WORKDIR / - -COPY --from=build /usr/share/zoneinfo /usr/share/zoneinfo -COPY --from=build /app /app - -# RUN ls -la / - -ENV TZ=Asia/Jakarta - -ENTRYPOINT ["/app"] diff --git a/Makefile b/Makefile index 56cf375..a24cc79 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,9 @@ -PROJECT_NAME = "enaklo-pos-backend" -DB_USERNAME := fortuna_admin -DB_PASSWORD := Z4G827t9428QFQ%5ESZXW%2343dB%25%214Bmh80 -DB_HOST := 103.96.146.124 -DB_PORT := 1960 -DB_NAME := fortuna-staging +#PROJECT_NAME = "enaklo-pos-backend" +DB_USERNAME :=apskel +DB_PASSWORD :=7a8UJbM2GgBWaseh0lnP3O5i1i5nINXk +DB_HOST :=62.72.45.250 +DB_PORT :=5433 +DB_NAME :=apskel_pos DB_URL = postgres://$(DB_USERNAME):$(DB_PASSWORD)@$(DB_HOST):$(DB_PORT)/$(DB_NAME)?sslmode=disable diff --git a/ORDER_VOID_STATUS_IMPROVEMENT.md b/ORDER_VOID_STATUS_IMPROVEMENT.md new file mode 100644 index 0000000..f0e45bb --- /dev/null +++ b/ORDER_VOID_STATUS_IMPROVEMENT.md @@ -0,0 +1,120 @@ +# Order Void Status Improvement + +## Overview + +This document describes the improved approach for handling order void status when all items are voided. + +## Problem with Previous Approach + +The previous implementation only set the `is_void` flag to `true` when voiding orders, but kept the original order status (e.g., "pending", "preparing", etc.). This approach had several issues: + +1. **Poor Semantic Meaning**: Orders with status "pending" but `is_void = true` were confusing +2. **Difficult Querying**: Hard to filter voided orders by status alone +3. **Inconsistent State**: Order status didn't reflect the actual business state +4. **Audit Trail Issues**: No clear indication of when and why orders were voided + +## Improved Approach + +### 1. Status Update Strategy + +When an order is voided (either entirely or when all items are voided), the system now: + +- **Sets `is_void = true`** (for audit trail and void-specific operations) +- **Updates `status = 'cancelled'`** (for business logic and semantic clarity) +- **Records void metadata** (reason, timestamp, user who voided) + +### 2. Benefits + +#### **Clear Semantic Meaning** +- Voided orders have status "cancelled" which clearly indicates they are no longer active +- Business logic can rely on status for workflow decisions +- Frontend can easily display voided orders with appropriate styling + +#### **Better Querying** +```sql +-- Find all cancelled/voided orders +SELECT * FROM orders WHERE status = 'cancelled'; + +-- Find all active orders (excluding voided) +SELECT * FROM orders WHERE status != 'cancelled'; + +-- Find voided orders with audit info +SELECT * FROM orders WHERE is_void = true; +``` + +#### **Consistent State Management** +- Order status always reflects the current business state +- No conflicting states (e.g., "pending" but voided) +- Easier to implement business rules and validations + +#### **Enhanced Audit Trail** +- `is_void` flag for void-specific operations +- `void_reason`, `voided_at`, `voided_by` for detailed audit +- `status = 'cancelled'` for business workflow + +### 3. Implementation Details + +#### **New Repository Method** +```go +VoidOrderWithStatus(ctx context.Context, id uuid.UUID, status entities.OrderStatus, reason string, voidedBy uuid.UUID) error +``` + +This method updates both status and void flags in a single atomic transaction. + +#### **Updated Processor Logic** +```go +// For "ALL" void type +if err := p.orderRepo.VoidOrderWithStatus(ctx, req.OrderID, entities.OrderStatusCancelled, req.Reason, voidedBy); err != nil { + return fmt.Errorf("failed to void order: %w", err) +} + +// For "ITEM" void type when all items are voided +if allItemsVoided { + if err := p.orderRepo.VoidOrderWithStatus(ctx, req.OrderID, entities.OrderStatusCancelled, req.Reason, voidedBy); err != nil { + return fmt.Errorf("failed to void order after all items voided: %w", err) + } +} +``` + +#### **Database Migration** +Added migration `000021_add_paid_status_to_orders.up.sql` to include "paid" status in the constraint. + +### 4. Status Flow + +``` +Order Created → pending + ↓ +Items Added/Modified → pending + ↓ +Order Processing → preparing → ready → completed + ↓ +Order Voided → cancelled (with is_void = true) +``` + +### 5. Backward Compatibility + +- Existing `is_void` flag is preserved for backward compatibility +- New approach is additive, not breaking +- Existing queries using `is_void` continue to work +- New queries can use `status = 'cancelled'` for better performance + +### 6. Best Practices + +#### **For Queries** +- Use `status = 'cancelled'` for business logic and filtering +- Use `is_void = true` for void-specific operations and audit trails +- Combine both when you need complete void information + +#### **For Business Logic** +- Check `status != 'cancelled'` before allowing modifications +- Use `is_void` flag for void-specific validations +- Always include void reason and user for audit purposes + +#### **For Frontend** +- Display cancelled orders with appropriate styling +- Show void reason and timestamp when available +- Disable actions on cancelled orders + +## Conclusion + +This improved approach provides better semantic meaning, easier querying, and more consistent state management while maintaining backward compatibility. The combination of status updates and void flags creates a robust system for handling order cancellations. \ No newline at end of file diff --git a/OUTLET_TAX_CALCULATION.md b/OUTLET_TAX_CALCULATION.md new file mode 100644 index 0000000..f3cd3d7 --- /dev/null +++ b/OUTLET_TAX_CALCULATION.md @@ -0,0 +1,155 @@ +# Outlet-Based Tax Calculation Implementation + +## Overview + +This document describes the implementation of outlet-based tax calculation in the order processing system. The system now uses the tax rate configured for each outlet instead of a hardcoded tax rate. + +## Feature Description + +Previously, the system used a hardcoded 10% tax rate for all orders. Now, the tax calculation is based on the `tax_rate` field configured for each outlet, allowing for different tax rates across different locations. + +## Implementation Details + +### 1. Order Processor Changes + +The `OrderProcessorImpl` has been updated to: + +- Accept an `OutletRepository` dependency +- Fetch outlet information to get the tax rate +- Calculate tax using the outlet's specific tax rate +- Recalculate tax when adding items to existing orders + +### 2. Tax Calculation Logic + +```go +// Get outlet information for tax rate +outlet, err := p.outletRepo.GetByID(ctx, req.OutletID) +if err != nil { + return nil, fmt.Errorf("outlet not found: %w", err) +} + +// Calculate tax using outlet's tax rate +taxAmount := subtotal * outlet.TaxRate +totalAmount := subtotal + taxAmount +``` + +### 3. Database Schema + +The `outlets` table includes: + +- `tax_rate`: Decimal field (DECIMAL(5,4)) for tax rate as a decimal (e.g., 0.085 for 8.5%) +- Constraint: `CHECK (tax_rate >= 0 AND tax_rate <= 1)` to ensure valid percentage + +### 4. Tax Rate Examples + +| Tax Rate (Decimal) | Percentage | Example Calculation | +|-------------------|------------|-------------------| +| 0.0000 | 0% | No tax | +| 0.0500 | 5% | $100 × 0.05 = $5.00 tax | +| 0.0850 | 8.5% | $100 × 0.085 = $8.50 tax | +| 0.1000 | 10% | $100 × 0.10 = $10.00 tax | +| 0.1500 | 15% | $100 × 0.15 = $15.00 tax | + +### 5. API Usage + +The tax calculation is automatic and transparent to the API consumer. When creating orders or adding items, the system: + +1. Fetches the outlet's tax rate +2. Calculates tax based on the current subtotal +3. Updates the order with the correct tax amount + +```json +{ + "outlet_id": "uuid-of-outlet", + "order_items": [ + { + "product_id": "uuid-of-product", + "quantity": 2 + } + ] +} +``` + +The response will include the calculated tax amount: + +```json +{ + "id": "order-uuid", + "outlet_id": "outlet-uuid", + "subtotal": 20.00, + "tax_amount": 1.70, // Based on outlet's tax rate + "total_amount": 21.70 +} +``` + +### 6. Business Scenarios + +#### Scenario 1: Different Tax Rates by Location +- **Downtown Location**: 8.5% tax rate +- **Suburban Location**: 6.5% tax rate +- **Airport Location**: 10.0% tax rate + +#### Scenario 2: Tax-Exempt Locations +- **Wholesale Outlet**: 0% tax rate +- **Export Zone**: 0% tax rate + +#### Scenario 3: Seasonal Tax Changes +- **Holiday Period**: Temporary tax rate adjustments +- **Promotional Period**: Reduced tax rates + +### 7. Validation + +The system includes several validation checks: + +1. **Outlet Existence**: Verifies the outlet exists +2. **Tax Rate Range**: Database constraint ensures 0% ≤ tax rate ≤ 100% +3. **Tax Calculation**: Ensures positive tax amounts + +### 8. Error Handling + +Common error scenarios: + +- `outlet not found`: When an invalid outlet ID is provided +- Database constraint violations for invalid tax rates + +### 9. Testing + +The implementation includes unit tests to verify: + +- Correct tax calculation with different outlet tax rates +- Proper error handling for invalid outlets +- Tax recalculation when adding items to existing orders + +### 10. Migration + +The feature uses existing database schema from migration `000002_create_outlets_table.up.sql` which includes the `tax_rate` column. + +### 11. Configuration + +Outlet tax rates can be configured through: + +1. **Outlet Creation API**: Set initial tax rate +2. **Outlet Update API**: Modify tax rate for existing outlets +3. **Database Direct Update**: For bulk changes + +### 12. Future Enhancements + +Potential improvements: + +1. **Tax Rate History**: Track tax rate changes over time +2. **Conditional Tax Rates**: Different rates based on order type or customer type +3. **Tax Exemptions**: Support for tax-exempt customers or items +4. **Multi-Tax Support**: Support for multiple tax types (state, local, etc.) +5. **Tax Rate Validation**: Integration with tax authority APIs for rate validation + +### 13. Performance Considerations + +- Outlet information is fetched once per order creation/modification +- Tax calculation is performed in memory for efficiency +- Consider caching outlet information for high-volume scenarios + +### 14. Compliance + +- Tax rates should comply with local tax regulations +- Consider implementing tax rate validation against official sources +- Maintain audit trails for tax rate changes \ No newline at end of file diff --git a/PRODUCT_STOCK_MANAGEMENT.md b/PRODUCT_STOCK_MANAGEMENT.md new file mode 100644 index 0000000..9b20be1 --- /dev/null +++ b/PRODUCT_STOCK_MANAGEMENT.md @@ -0,0 +1,157 @@ +# Product Stock Management + +This document explains the new product stock management functionality that allows automatic inventory record creation when products are created or updated. + +## Features + +1. **Automatic Inventory Creation**: When creating a product, you can automatically create inventory records for all outlets in the organization +2. **Initial Stock Setting**: Set initial stock quantity for all outlets +3. **Reorder Level Management**: Set reorder levels for all outlets +4. **Bulk Inventory Updates**: Update reorder levels for all existing inventory records when updating a product + +## API Usage + +### Creating a Product with Stock Management + +```json +POST /api/v1/products +{ + "category_id": "550e8400-e29b-41d4-a716-446655440000", + "name": "Premium Coffee", + "description": "High-quality coffee beans", + "price": 15.99, + "cost": 8.50, + "business_type": "restaurant", + "is_active": true, + "variants": [ + { + "name": "Large", + "price_modifier": 2.00, + "cost": 1.00 + } + ], + "initial_stock": 100, + "reorder_level": 20, + "create_inventory": true +} +``` + +**Parameters:** +- `initial_stock` (optional): Initial stock quantity for all outlets (default: 0) +- `reorder_level` (optional): Reorder level for all outlets (default: 0) +- `create_inventory` (optional): Whether to create inventory records for all outlets (default: false) + +### Updating a Product with Stock Management + +```json +PUT /api/v1/products/{product_id} +{ + "name": "Premium Coffee Updated", + "price": 16.99, + "reorder_level": 25 +} +``` + +**Parameters:** +- `reorder_level` (optional): Updates the reorder level for all existing inventory records + +## How It Works + +### Product Creation Flow + +1. **Validation**: Validates product data and checks for duplicates +2. **Product Creation**: Creates the product in the database +3. **Variant Creation**: Creates product variants if provided +4. **Inventory Creation** (if `create_inventory: true`): + - Fetches all outlets for the organization + - Creates inventory records for each outlet with: + - Initial stock quantity (if provided) + - Reorder level (if provided) + - Uses bulk creation for efficiency + +### Product Update Flow + +1. **Validation**: Validates update data +2. **Product Update**: Updates the product in the database +3. **Inventory Update** (if `reorder_level` provided): + - Fetches all existing inventory records for the product + - Updates reorder level for each inventory record + +## Database Schema + +### Products Table +- Standard product fields +- No changes to existing schema + +### Inventory Table +- `outlet_id`: Reference to outlet +- `product_id`: Reference to product +- `quantity`: Current stock quantity +- `reorder_level`: Reorder threshold +- `updated_at`: Last update timestamp + +## Error Handling + +- **No Outlets**: If `create_inventory: true` but no outlets exist, returns an error +- **Duplicate Inventory**: Prevents creating duplicate inventory records for the same product-outlet combination +- **Validation**: Validates stock quantities and reorder levels are non-negative + +## Performance Considerations + +- **Bulk Operations**: Uses `CreateInBatches` for efficient bulk inventory creation +- **Transactions**: Inventory operations are wrapped in transactions for data consistency +- **Batch Size**: Default batch size of 100 for bulk operations + +## Example Response + +```json +{ + "id": "550e8400-e29b-41d4-a716-446655440001", + "organization_id": "550e8400-e29b-41d4-a716-446655440000", + "category_id": "550e8400-e29b-41d4-a716-446655440002", + "name": "Premium Coffee", + "description": "High-quality coffee beans", + "price": 15.99, + "cost": 8.50, + "business_type": "restaurant", + "is_active": true, + "created_at": "2024-01-15T10:30:00Z", + "updated_at": "2024-01-15T10:30:00Z", + "category": { + "id": "550e8400-e29b-41d4-a716-446655440002", + "name": "Beverages" + }, + "variants": [ + { + "id": "550e8400-e29b-41d4-a716-446655440003", + "name": "Large", + "price_modifier": 2.00, + "cost": 1.00 + } + ], + "inventory": [ + { + "id": "550e8400-e29b-41d4-a716-446655440004", + "outlet_id": "550e8400-e29b-41d4-a716-446655440005", + "quantity": 100, + "reorder_level": 20 + }, + { + "id": "550e8400-e29b-41d4-a716-446655440006", + "outlet_id": "550e8400-e29b-41d4-a716-446655440007", + "quantity": 100, + "reorder_level": 20 + } + ] +} +``` + +## Migration Notes + +This feature requires the existing database schema with: +- `products` table +- `inventory` table +- `outlets` table +- Proper foreign key relationships + +No additional migrations are required as the feature uses existing tables. \ No newline at end of file diff --git a/PRODUCT_VARIANT_PRICE_MODIFIER.md b/PRODUCT_VARIANT_PRICE_MODIFIER.md new file mode 100644 index 0000000..97a8217 --- /dev/null +++ b/PRODUCT_VARIANT_PRICE_MODIFIER.md @@ -0,0 +1,127 @@ +# Product Variant Price Modifier Implementation + +## Overview + +This document describes the implementation of price modifier functionality for product variants in the order processing system. + +## Feature Description + +When a product variant is specified in an order item, the system now automatically applies the variant's price modifier to the base product price. This allows for flexible pricing based on product variations (e.g., size upgrades, add-ons, etc.). + +## Implementation Details + +### 1. Order Processor Changes + +The `OrderProcessorImpl` has been updated to: + +- Accept a `ProductVariantRepository` dependency +- Fetch product variant information when `ProductVariantID` is provided +- Apply the price modifier to the base product price +- Use variant-specific cost if available + +### 2. Price Calculation Logic + +```go +// Base price from product +unitPrice := product.Price +unitCost := product.Cost + +// Apply variant price modifier if specified +if itemReq.ProductVariantID != nil { + variant, err := p.productVariantRepo.GetByID(ctx, *itemReq.ProductVariantID) + if err != nil { + return nil, fmt.Errorf("product variant not found: %w", err) + } + + // Verify variant belongs to the product + if variant.ProductID != itemReq.ProductID { + return nil, fmt.Errorf("product variant does not belong to the specified product") + } + + // Apply price modifier + unitPrice += variant.PriceModifier + + // Use variant cost if available, otherwise use product cost + if variant.Cost > 0 { + unitCost = variant.Cost + } +} +``` + +### 3. Database Schema + +The `product_variants` table includes: + +- `price_modifier`: Decimal field for price adjustments (+/- values) +- `cost`: Optional variant-specific cost +- `product_id`: Foreign key to products table + +### 4. API Usage + +When creating orders or adding items to existing orders, you can specify a product variant: + +```json +{ + "order_items": [ + { + "product_id": "uuid-of-product", + "product_variant_id": "uuid-of-variant", + "quantity": 2, + "notes": "Extra large size" + } + ] +} +``` + +### 5. Example Scenarios + +#### Scenario 1: Size Upgrade +- Base product: Coffee ($3.00) +- Variant: Large (+$1.00 modifier) +- Final price: $4.00 + +#### Scenario 2: Add-on +- Base product: Pizza ($12.00) +- Variant: Extra cheese (+$2.50 modifier) +- Final price: $14.50 + +#### Scenario 3: Discount +- Base product: Sandwich ($8.00) +- Variant: Student discount (-$1.00 modifier) +- Final price: $7.00 + +## Validation + +The system includes several validation checks: + +1. **Variant Existence**: Verifies the product variant exists +2. **Product Association**: Ensures the variant belongs to the specified product +3. **Price Integrity**: Maintains positive pricing (base price + modifier must be >= 0) + +## Error Handling + +Common error scenarios: + +- `product variant not found`: When an invalid variant ID is provided +- `product variant does not belong to the specified product`: When variant-product mismatch occurs + +## Testing + +The implementation includes unit tests to verify: + +- Correct price calculation with variants +- Proper error handling for invalid variants +- Cost calculation using variant-specific costs + +## Migration + +The feature uses existing database schema from migration `000013_add_cost_to_product_variants.up.sql` which adds the `cost` column to the `product_variants` table. + +## Future Enhancements + +Potential improvements: + +1. **Percentage-based modifiers**: Support for percentage-based price adjustments +2. **Conditional modifiers**: Modifiers that apply based on order context +3. **Bulk variant pricing**: Tools for managing variant pricing across products +4. **Pricing history**: Track price modifier changes over time \ No newline at end of file diff --git a/PROFIT_LOSS_ANALYTICS_API.md b/PROFIT_LOSS_ANALYTICS_API.md new file mode 100644 index 0000000..a86e1d7 --- /dev/null +++ b/PROFIT_LOSS_ANALYTICS_API.md @@ -0,0 +1,241 @@ +# Profit/Loss Analytics API + +This document describes the Profit/Loss Analytics API that provides comprehensive financial analysis for the POS system, including revenue, costs, and profitability metrics. + +## Overview + +The Profit/Loss Analytics API allows you to: +- Analyze profit and loss performance over time periods +- Track gross profit and net profit margins +- View product-wise profitability +- Monitor cost vs revenue trends +- Calculate profitability ratios + +## Authentication + +All analytics endpoints require authentication and admin/manager permissions. + +## Endpoints + +### Get Profit/Loss Analytics + +**Endpoint:** `GET /api/v1/analytics/profit-loss` + +**Description:** Retrieves comprehensive profit and loss analytics data including summary metrics, time-series data, and product profitability analysis. + +**Query Parameters:** +- `outlet_id` (UUID, optional) - Filter by specific outlet +- `date_from` (string, required) - Start date in DD-MM-YYYY format +- `date_to` (string, required) - End date in DD-MM-YYYY format +- `group_by` (string, optional) - Time grouping: `hour`, `day`, `week`, `month` (default: `day`) + +**Example Request:** +```bash +curl -X GET "http://localhost:8080/api/v1/analytics/profit-loss?date_from=01-12-2023&date_to=31-12-2023&group_by=day" \ + -H "Authorization: Bearer " \ + -H "Organization-ID: " +``` + +**Example Response:** +```json +{ + "success": true, + "data": { + "organization_id": "123e4567-e89b-12d3-a456-426614174000", + "outlet_id": "123e4567-e89b-12d3-a456-426614174001", + "date_from": "2023-12-01T00:00:00Z", + "date_to": "2023-12-31T23:59:59Z", + "group_by": "day", + "summary": { + "total_revenue": 125000.00, + "total_cost": 75000.00, + "gross_profit": 50000.00, + "gross_profit_margin": 40.00, + "total_tax": 12500.00, + "total_discount": 2500.00, + "net_profit": 35000.00, + "net_profit_margin": 28.00, + "total_orders": 1250, + "average_profit": 28.00, + "profitability_ratio": 66.67 + }, + "data": [ + { + "date": "2023-12-01T00:00:00Z", + "revenue": 4032.26, + "cost": 2419.35, + "gross_profit": 1612.91, + "gross_profit_margin": 40.00, + "tax": 403.23, + "discount": 80.65, + "net_profit": 1129.03, + "net_profit_margin": 28.00, + "orders": 40 + }, + { + "date": "2023-12-02T00:00:00Z", + "revenue": 3750.00, + "cost": 2250.00, + "gross_profit": 1500.00, + "gross_profit_margin": 40.00, + "tax": 375.00, + "discount": 75.00, + "net_profit": 1050.00, + "net_profit_margin": 28.00, + "orders": 35 + } + ], + "product_data": [ + { + "product_id": "123e4567-e89b-12d3-a456-426614174002", + "product_name": "Premium Burger", + "category_id": "123e4567-e89b-12d3-a456-426614174003", + "category_name": "Main Course", + "quantity_sold": 150, + "revenue": 2250.00, + "cost": 900.00, + "gross_profit": 1350.00, + "gross_profit_margin": 60.00, + "average_price": 15.00, + "average_cost": 6.00, + "profit_per_unit": 9.00 + }, + { + "product_id": "123e4567-e89b-12d3-a456-426614174004", + "product_name": "Caesar Salad", + "category_id": "123e4567-e89b-12d3-a456-426614174005", + "category_name": "Salads", + "quantity_sold": 80, + "revenue": 960.00, + "cost": 384.00, + "gross_profit": 576.00, + "gross_profit_margin": 60.00, + "average_price": 12.00, + "average_cost": 4.80, + "profit_per_unit": 7.20 + } + ] + } +} +``` + +## Response Structure + +### Summary Object +- `total_revenue` - Total revenue for the period +- `total_cost` - Total cost of goods sold +- `gross_profit` - Revenue minus cost (total_revenue - total_cost) +- `gross_profit_margin` - Gross profit as percentage of revenue +- `total_tax` - Total tax collected +- `total_discount` - Total discounts given +- `net_profit` - Profit after taxes and discounts +- `net_profit_margin` - Net profit as percentage of revenue +- `total_orders` - Number of completed orders +- `average_profit` - Average profit per order +- `profitability_ratio` - Gross profit as percentage of total cost + +### Time Series Data +The `data` array contains profit/loss metrics grouped by the specified time period: +- `date` - Date/time for the data point +- `revenue` - Revenue for the period +- `cost` - Cost for the period +- `gross_profit` - Gross profit for the period +- `gross_profit_margin` - Gross profit margin percentage +- `tax` - Tax amount for the period +- `discount` - Discount amount for the period +- `net_profit` - Net profit for the period +- `net_profit_margin` - Net profit margin percentage +- `orders` - Number of orders in the period + +### Product Profitability Data +The `product_data` array shows the top 20 most profitable products: +- `product_id` - Unique product identifier +- `product_name` - Product name +- `category_id` - Product category identifier +- `category_name` - Category name +- `quantity_sold` - Total units sold +- `revenue` - Total revenue from the product +- `cost` - Total cost for the product +- `gross_profit` - Total gross profit +- `gross_profit_margin` - Profit margin percentage +- `average_price` - Average selling price per unit +- `average_cost` - Average cost per unit +- `profit_per_unit` - Average profit per unit + +## Key Metrics Explained + +### Gross Profit Margin +Calculated as: `(Revenue - Cost) / Revenue × 100` +Shows the percentage of revenue retained after direct costs. + +### Net Profit Margin +Calculated as: `(Revenue - Cost - Discount) / Revenue × 100` +Shows the percentage of revenue retained after all direct costs and discounts. + +### Profitability Ratio +Calculated as: `Gross Profit / Total Cost × 100` +Shows the return on investment for costs incurred. + +## Use Cases + +1. **Financial Performance Analysis** - Track overall profitability trends +2. **Product Performance** - Identify most and least profitable products +3. **Cost Management** - Monitor cost ratios and margins +4. **Pricing Strategy** - Analyze impact of pricing on profitability +5. **Inventory Decisions** - Focus on high-margin products +6. **Business Intelligence** - Make data-driven financial decisions + +## Error Responses + +The API returns standard error responses with appropriate HTTP status codes: + +**400 Bad Request:** +```json +{ + "success": false, + "errors": [ + { + "code": "invalid_request", + "entity": "AnalyticsHandler::GetProfitLossAnalytics", + "message": "date_from is required" + } + ] +} +``` + +**401 Unauthorized:** +```json +{ + "success": false, + "errors": [ + { + "code": "unauthorized", + "entity": "AuthMiddleware", + "message": "Invalid or missing authentication token" + } + ] +} +``` + +**403 Forbidden:** +```json +{ + "success": false, + "errors": [ + { + "code": "forbidden", + "entity": "AuthMiddleware", + "message": "Admin or manager role required" + } + ] +} +``` + +## Notes + +- Only completed and paid orders are included in profit/loss calculations +- Voided and refunded orders are excluded from the analysis +- Product profitability is sorted by gross profit in descending order +- Time series data is automatically filled for periods with no data (showing zero values) +- All monetary values are in the organization's base currency +- Margins and ratios are calculated as percentages with 2 decimal precision \ No newline at end of file diff --git a/README.md b/README.md index 51e72ac..6623a96 100644 --- a/README.md +++ b/README.md @@ -57,8 +57,254 @@ $ ./bin/http-server --env-path ./config/env/.env ``` ## API Docs -* [enaklo-pos Backend](https://enaklo-pos-be.app-dev.altru.id/docs/index.html#/) +* [apskel-pos Backend](https://apskel-pos-be.app-dev.altru.id/docs/index.html#/) ## License -This project is licensed under the [MIT License](https://github.com/pvarentsov/enaklo-pos-be/blob/main/LICENSE). +This project is licensed under the [MIT License](https://github.com/pvarentsov/apskel-pos-be/blob/main/LICENSE). + +# Apskel POS Backend + +A SaaS Point of Sale (POS) Restaurant System backend built with clean architecture principles in Go. + +## Architecture Overview + +This application follows a clean architecture pattern with clear separation of concerns: + +``` +Handler → Service → Processor → Repository +``` + +### Layers + +1. **Contract Package** (`internal/contract/`) + - Request/Response DTOs for API communication + - Contains JSON tags for serialization + - Input validation tags + +2. **Handler Layer** (`internal/handler/`) + - HTTP request/response handling + - Request validation using go-playground/validator + - Route definitions and middleware + - Transforms contracts to/from services + +3. **Service Layer** (`internal/service/`) + - Business logic orchestration + - Calls processors and transformers + - Coordinates between different business operations + +4. **Processor Layer** (`internal/processor/`) + - Complex business operations + - Cross-repository transactions + - Business rule enforcement + - Handles operations like order creation with inventory updates + +5. **Repository Layer** (`internal/repository/`) + - Data access layer + - Individual repository per entity + - Database-specific operations + - Uses entities for database models + +6. **Supporting Packages**: + - **Models** (`internal/models/`) - Pure business logic models (no database dependencies) + - **Entities** (`internal/entities/`) - Database models with GORM tags + - **Constants** (`internal/constants/`) - Type-safe enums and business constants + - **Transformer** (`internal/transformer/`) - Contract ↔ Model conversions + - **Mappers** (`internal/mappers/`) - Model ↔ Entity conversions + +## Key Features + +- **Clean Architecture**: Strict separation between business logic and infrastructure +- **Type Safety**: Constants package with validation helpers +- **Validation**: Comprehensive request validation using go-playground/validator +- **Error Handling**: Structured error responses with proper HTTP status codes +- **Database Independence**: Business logic never depends on database implementation +- **Testability**: Each layer can be tested independently + +## API Endpoints + +### Health Check +- `GET /api/v1/health` - Health check endpoint + +### Organizations +- `POST /api/v1/organizations` - Create organization +- `GET /api/v1/organizations` - List organizations +- `GET /api/v1/organizations/{id}` - Get organization by ID +- `PUT /api/v1/organizations/{id}` - Update organization +- `DELETE /api/v1/organizations/{id}` - Delete organization + +### Users +- `POST /api/v1/users` - Create user +- `GET /api/v1/users` - List users +- `GET /api/v1/users/{id}` - Get user by ID +- `PUT /api/v1/users/{id}` - Update user +- `DELETE /api/v1/users/{id}` - Delete user +- `PUT /api/v1/users/{id}/password` - Change password +- `PUT /api/v1/users/{id}/activate` - Activate user +- `PUT /api/v1/users/{id}/deactivate` - Deactivate user + +### Orders +- `POST /api/v1/orders` - Create order with items +- `GET /api/v1/orders` - List orders +- `GET /api/v1/orders/{id}` - Get order by ID +- `GET /api/v1/orders/{id}?include_items=true` - Get order with items +- `PUT /api/v1/orders/{id}` - Update order +- `PUT /api/v1/orders/{id}/cancel` - Cancel order +- `PUT /api/v1/orders/{id}/complete` - Complete order +- `POST /api/v1/orders/{id}/items` - Add item to order + +### Order Items +- `PUT /api/v1/order-items/{id}` - Update order item +- `DELETE /api/v1/order-items/{id}` - Remove order item + +## Installation + +1. **Clone the repository** + ```bash + git clone + cd apskel-pos-backend + ``` + +2. **Install dependencies** + ```bash + go mod tidy + ``` + +3. **Set up database** + ```bash + # Set your PostgreSQL database URL + export DATABASE_URL="postgres://username:password@localhost:5432/apskel_pos?sslmode=disable" + ``` + +4. **Run migrations** + ```bash + make migration-up + ``` + +## Usage + +### Development + +```bash +# Start the server +go run cmd/server/main.go -port 8080 -db-url "postgres://username:password@localhost:5432/apskel_pos?sslmode=disable" + +# Or using environment variable +export DATABASE_URL="postgres://username:password@localhost:5432/apskel_pos?sslmode=disable" +go run cmd/server/main.go -port 8080 +``` + +### Using Make Commands + +```bash +# Run the application +make start + +# Format code +make fmt + +# Run tests +make test + +# Build for production +make build-http + +# Docker operations +make docker-up +make docker-down + +# Database migrations +make migration-create name=create_users_table +make migration-up +make migration-down +``` + +## Example API Usage + +### Create Organization +```bash +curl -X POST http://localhost:8080/api/v1/organizations \ + -H "Content-Type: application/json" \ + -d '{ + "name": "My Restaurant", + "plan_type": "premium" + }' +``` + +### Create User +```bash +curl -X POST http://localhost:8080/api/v1/users \ + -H "Content-Type: application/json" \ + -d '{ + "organization_id": "uuid-here", + "username": "john_doe", + "email": "john@example.com", + "password": "password123", + "full_name": "John Doe", + "role": "manager" + }' +``` + +### Create Order with Items +```bash +curl -X POST http://localhost:8080/api/v1/orders \ + -H "Content-Type: application/json" \ + -d '{ + "outlet_id": "uuid-here", + "user_id": "uuid-here", + "table_number": "A1", + "order_type": "dine_in", + "notes": "No onions", + "order_items": [ + { + "product_id": "uuid-here", + "quantity": 2, + "unit_price": 15.99 + } + ] + }' +``` + +## Project Structure + +``` +apskel-pos-backend/ +├── cmd/ +│ └── server/ # Application entry point +├── internal/ +│ ├── app/ # Application wiring and dependency injection +│ ├── contract/ # API contracts (request/response DTOs) +│ ├── handler/ # HTTP handlers and routes +│ ├── service/ # Business logic orchestration +│ ├── processor/ # Complex business operations +│ ├── repository/ # Data access layer +│ ├── models/ # Pure business models +│ ├── entities/ # Database entities (GORM models) +│ ├── constants/ # Business constants and enums +│ ├── transformer/ # Contract ↔ Model transformations +│ └── mappers/ # Model ↔ Entity transformations +├── migrations/ # Database migrations +├── Makefile # Build and development commands +├── go.mod # Go module definition +└── README.md # This file +``` + +## Dependencies + +- **[Gorilla Mux](https://github.com/gorilla/mux)** - HTTP router and URL matcher +- **[GORM](https://gorm.io/)** - ORM for database operations +- **[PostgreSQL Driver](https://github.com/lib/pq)** - PostgreSQL database driver +- **[Validator](https://github.com/go-playground/validator)** - Struct validation +- **[UUID](https://github.com/google/uuid)** - UUID generation and parsing + +## Contributing + +1. Fork the repository +2. Create a feature branch +3. Commit your changes +4. Push to the branch +5. Create a Pull Request + +## License + +This project is licensed under the MIT License - see the LICENSE file for details. diff --git a/cmd/server/main.go b/cmd/server/main.go new file mode 100644 index 0000000..d99e6f0 --- /dev/null +++ b/cmd/server/main.go @@ -0,0 +1,30 @@ +package main + +import ( + "apskel-pos-be/config" + "apskel-pos-be/internal/app" + "apskel-pos-be/internal/db" + "apskel-pos-be/internal/logger" + "log" +) + +func main() { + cfg := config.LoadConfig() + logger.Setup(cfg.LogLevel(), cfg.LogFormat()) + + db, err := db.NewPostgres(cfg.Database) + if err != nil { + log.Fatal(err) + } + + logger.NonContext.Info("helloworld") + application := app.NewApp(db) + + if err := application.Initialize(cfg); err != nil { + log.Fatalf("Failed to initialize application: %v", err) + } + + if err := application.Start(cfg.Port()); err != nil { + log.Fatalf("Failed to start server: %v", err) + } +} diff --git a/config/brevo.go b/config/brevo.go deleted file mode 100644 index 41facd5..0000000 --- a/config/brevo.go +++ /dev/null @@ -1,9 +0,0 @@ -package config - -type Brevo struct { - APIKey string `mapstructure:"api_key"` -} - -func (b *Brevo) GetApiKey() string { - return b.APIKey -} diff --git a/config/configs.go b/config/configs.go index 02f03c8..6a6c529 100644 --- a/config/configs.go +++ b/config/configs.go @@ -10,7 +10,7 @@ import ( ) const ( - YAML_PATH = "infra/enaklopos.%s" + YAML_PATH = "infra/%s" ENV_MODE = "ENV_MODE" DEFAULT_ENV_MODE = "development" ) @@ -24,18 +24,11 @@ var ( ) type Config struct { - Server Server `mapstructure:"server"` - Database Database `mapstructure:"postgresql"` - Jwt Jwt `mapstructure:"jwt"` - OSSConfig OSSConfig `mapstructure:"oss"` - Midtrans Midtrans `mapstructure:"midtrans"` - Brevo Brevo `mapstructure:"brevo"` - Email Email `mapstructure:"email"` - Withdraw Withdraw `mapstructure:"withdrawal"` - Discovery Discovery `mapstructure:"discovery"` - Order Order `mapstructure:"order"` - FeatureToggle FeatureToggle `mapstructure:"feature_toggle"` - LinkQu LinkQu `mapstructure:"linkqu"` + Server Server `mapstructure:"server"` + Database Database `mapstructure:"postgresql"` + Jwt Jwt `mapstructure:"jwt"` + Log Log `mapstructure:"log"` + S3Config S3Config `mapstructure:"s3"` } var ( @@ -46,7 +39,7 @@ var ( func LoadConfig() *Config { envMode := os.Getenv(ENV_MODE) if _, ok := validEnvMode[envMode]; !ok { - envMode = DEFAULT_ENV_MODE // default env mode + envMode = DEFAULT_ENV_MODE } cfgFilePath := fmt.Sprintf(YAML_PATH, envMode) @@ -72,19 +65,17 @@ func (c *Config) Auth() *AuthConfig { return &AuthConfig{ jwtTokenSecret: c.Jwt.Token.Secret, jwtTokenExpiresTTL: c.Jwt.Token.ExpiresTTL, - jwtOrderSecret: c.Jwt.TokenOrder.Secret, - jwtOrderExpiresTTL: c.Jwt.TokenOrder.ExpiresTTL, - jwtSecretResetPassword: JWT{ - secret: c.Jwt.TokenResetPassword.Secret, - expireTTL: c.Jwt.TokenResetPassword.ExpiresTTL, - }, - jwtWithdraw: JWT{ - secret: c.Jwt.TokenWithdraw.Secret, - expireTTL: c.Jwt.TokenWithdraw.ExpiresTTL, - }, - jwtCustomer: JWT{ - secret: c.Jwt.TokenCustomer.Secret, - expireTTL: c.Jwt.TokenCustomer.ExpiresTTL, - }, } } + +func (c *Config) LogLevel() string { + return c.Log.LogLevel +} + +func (c *Config) Port() string { + return c.Server.Port +} + +func (c *Config) LogFormat() string { + return c.Log.LogFormat +} diff --git a/config/crypto.go b/config/crypto.go index 8c28839..e3e90c5 100644 --- a/config/crypto.go +++ b/config/crypto.go @@ -3,13 +3,8 @@ package config import "time" type AuthConfig struct { - jwtTokenExpiresTTL int - jwtTokenSecret string - jwtOrderSecret string - jwtOrderExpiresTTL int - jwtSecretResetPassword JWT - jwtWithdraw JWT - jwtCustomer JWT + jwtTokenExpiresTTL int + jwtTokenSecret string } type JWT struct { @@ -21,38 +16,7 @@ func (c *AuthConfig) AccessTokenSecret() string { return c.jwtTokenSecret } -func (c *AuthConfig) AccessTokenOrderSecret() string { - return c.jwtOrderSecret -} - -func (c *AuthConfig) AccessTokenCustomerSecret() string { - return c.jwtCustomer.secret -} - -func (c *AuthConfig) AccessTokenWithdrawSecret() string { - return c.jwtWithdraw.secret -} - -func (c *AuthConfig) AccessTokenWithdrawExpire() time.Time { - duration := time.Duration(c.jwtWithdraw.expireTTL) - return time.Now().UTC().Add(time.Minute * duration) -} - -func (c *AuthConfig) AccessTokenOrderExpiresDate() time.Time { - duration := time.Duration(c.jwtOrderExpiresTTL) - return time.Now().UTC().Add(time.Minute * duration) -} - func (c *AuthConfig) AccessTokenExpiresDate() time.Time { duration := time.Duration(c.jwtTokenExpiresTTL) return time.Now().UTC().Add(time.Minute * duration) } - -func (c *AuthConfig) AccessTokenResetPasswordSecret() string { - return c.jwtSecretResetPassword.secret -} - -func (c *AuthConfig) AccessTokenResetPasswordExpire() time.Time { - duration := time.Duration(c.jwtSecretResetPassword.expireTTL) - return time.Now().UTC().Add(time.Minute * duration) -} diff --git a/config/discovery.go b/config/discovery.go deleted file mode 100644 index da479df..0000000 --- a/config/discovery.go +++ /dev/null @@ -1,17 +0,0 @@ -package config - -type Discovery struct { - ExploreDestinations []ExploreDestination `mapstructure:"explore_destinations"` - ExploreRegions []ExploreRegion `mapstructure:"explore_regions"` -} - -type ExploreDestinations []ExploreDestination - -type ExploreDestination struct { - Name string `mapstructure:"name"` - ImageURL string `mapstructure:"image_url"` -} - -type ExploreRegion struct { - Name string `mapstructure:"name"` -} diff --git a/config/email.go b/config/email.go deleted file mode 100644 index ace6a71..0000000 --- a/config/email.go +++ /dev/null @@ -1,34 +0,0 @@ -package config - -type Email struct { - Sender string `mapstructure:"sender"` - SenderCustomer string `mapstructure:"sender_customer"` - CustomReceiver string `mapstructure:"custom_receiver"` - ResetPassword EmailConfig `mapstructure:"reset_password"` -} - -type EmailConfig struct { - Subject string `mapstructure:"subject"` - OpeningWord string `mapstructure:"opening_word"` - Link string `mapstructure:"link"` - Notes string `mapstructure:"note"` - ClosingWord string `mapstructure:"closing_word"` - TemplateName string `mapstructure:"template_name"` - TemplatePath string `mapstructure:"template_path"` - TemplatePathCustomer string `mapstructure:"template_path_customer"` -} - -type EmailMemberRequestActionConfig struct { - TemplateName string `mapstructure:"template_name"` - TemplatePath string `mapstructure:"template_path"` - Subject string `mapstructure:"subject"` - Content string `mapstructure:"content"` -} - -func (e *Email) GetSender() string { - return e.Sender -} - -func (e *Email) GetCustomReceiver() string { - return e.CustomReceiver -} diff --git a/config/jwt.go b/config/jwt.go index d5b27da..aec29b2 100644 --- a/config/jwt.go +++ b/config/jwt.go @@ -1,11 +1,7 @@ package config type Jwt struct { - Token Token `mapstructure:"token"` - TokenOrder Token `mapstructure:"token-order"` - TokenResetPassword Token `mapstructure:"token-reset-password"` - TokenWithdraw Token `mapstructure:"token-withdraw"` - TokenCustomer Token `mapstructure:"token-customer"` + Token Token `mapstructure:"token"` } type Token struct { diff --git a/config/linqu.go b/config/linqu.go deleted file mode 100644 index ba37c6c..0000000 --- a/config/linqu.go +++ /dev/null @@ -1,49 +0,0 @@ -package config - -type LinkQu struct { - BaseURL string `mapstructure:"base_url"` - ClientID string `mapstructure:"client_id"` - ClientSecret string `mapstructure:"client_secret"` - SignatureKey string `mapstructure:"signature_key"` - Username string `mapstructure:"username"` - PIN string `mapstructure:"pin"` - CallbackURL string `mapstructure:"callback_url"` -} - -type LinkQuConfig interface { - LinkQuBaseURL() string - LinkQuClientID() string - LinkQuClientSecret() string - LinkQuSignatureKey() string - LinkQuUsername() string - LinkQuPIN() string - LinkQuCallbackURL() string -} - -func (c *LinkQu) LinkQuBaseURL() string { - return c.BaseURL -} - -func (c *LinkQu) LinkQuClientID() string { - return c.ClientID -} - -func (c *LinkQu) LinkQuClientSecret() string { - return c.ClientSecret -} - -func (c *LinkQu) LinkQuSignatureKey() string { - return c.SignatureKey -} - -func (c *LinkQu) LinkQuUsername() string { - return c.Username -} - -func (c *LinkQu) LinkQuPIN() string { - return c.PIN -} - -func (c *LinkQu) LinkQuCallbackURL() string { - return c.CallbackURL -} diff --git a/config/log.go b/config/log.go new file mode 100644 index 0000000..3d33c16 --- /dev/null +++ b/config/log.go @@ -0,0 +1,6 @@ +package config + +type Log struct { + LogFormat string `mapstructure:"log_format"` + LogLevel string `mapstructure:"log_level"` +} diff --git a/config/logger.go b/config/logger.go deleted file mode 100644 index d47b984..0000000 --- a/config/logger.go +++ /dev/null @@ -1,9 +0,0 @@ -package config - -type FeatureToggle struct { - LoggerEnabled bool `mapstructure:"logger_enabled"` -} - -func (f *FeatureToggle) IsLoggerEnabled() bool { - return f.LoggerEnabled -} diff --git a/config/midtrans.go b/config/midtrans.go deleted file mode 100644 index bddf763..0000000 --- a/config/midtrans.go +++ /dev/null @@ -1,25 +0,0 @@ -package config - -type Midtrans struct { - Serverkey string `mapstructure:"server_key"` - Clientkey string `mapstructure:"client_key"` - Env int `mapstructure:"env"` -} - -type MidtransConfig interface { - MidtransServerKey() string - MidtransClientKey() string - MidtranEnvType() int -} - -func (c *Midtrans) MidtransServerKey() string { - return c.Serverkey -} - -func (c *Midtrans) MidtransClientKey() string { - return c.Clientkey -} - -func (c *Midtrans) MidtranEnvType() int { - return c.Env -} diff --git a/config/order.go b/config/order.go deleted file mode 100644 index b2d7491..0000000 --- a/config/order.go +++ /dev/null @@ -1,12 +0,0 @@ -package config - -type Order struct { - Fee float64 `mapstructure:"fee"` -} - -func (w *Order) GetOrderFee(source string) float64 { - if source == "POS" { - return 0 - } - return w.Fee -} diff --git a/config/oss.go b/config/s3.go similarity index 62% rename from config/oss.go rename to config/s3.go index 477ecb4..89b7ccb 100644 --- a/config/oss.go +++ b/config/s3.go @@ -1,6 +1,6 @@ package config -type OSSConfig struct { +type S3Config struct { AccessKeyID string `mapstructure:"access_key_id"` AccessKeySecret string `mapstructure:"access_key_secret"` Endpoint string `mapstructure:"endpoint"` @@ -10,30 +10,30 @@ type OSSConfig struct { HostURL string `mapstructure:"host_url"` } -func (c OSSConfig) GetAccessKeyID() string { +func (c S3Config) GetAccessKeyID() string { return c.AccessKeyID } -func (c OSSConfig) GetAccessKeySecret() string { +func (c S3Config) GetAccessKeySecret() string { return c.AccessKeySecret } -func (c OSSConfig) GetEndpoint() string { +func (c S3Config) GetEndpoint() string { return c.Endpoint } -func (c OSSConfig) GetBucketName() string { +func (c S3Config) GetBucketName() string { return c.BucketName } -func (c OSSConfig) GetLogLevel() string { +func (c S3Config) GetLogLevel() string { return c.LogLevel } -func (c OSSConfig) GetHostURL() string { +func (c S3Config) GetHostURL() string { return c.HostURL } -func (c OSSConfig) GetPhotoFolder() string { +func (c S3Config) GetPhotoFolder() string { return c.PhotoFolder } diff --git a/config/tuya.go b/config/tuya.go deleted file mode 100644 index d912156..0000000 --- a/config/tuya.go +++ /dev/null @@ -1 +0,0 @@ -package config diff --git a/config/withdraw.go b/config/withdraw.go deleted file mode 100644 index 6014d63..0000000 --- a/config/withdraw.go +++ /dev/null @@ -1,9 +0,0 @@ -package config - -type Withdraw struct { - PlatformFee int64 `mapstructure:"platform_fee"` -} - -func (w *Withdraw) GetPlatformFee() int64 { - return w.PlatformFee -} diff --git a/docker-build.sh b/docker-build.sh new file mode 100755 index 0000000..e260c57 --- /dev/null +++ b/docker-build.sh @@ -0,0 +1,199 @@ +#!/bin/bash + +# Docker build script for apskel-pos-backend + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Functions +log_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +log_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +log_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Help function +show_help() { + echo "Usage: $0 [OPTION]" + echo "Build and manage Docker containers for apskel-pos-backend" + echo "" + echo "Options:" + echo " build Build the Docker image" + echo " run Run the production container" + echo " dev Run development environment with live reload" + echo " migrate Run database migrations" + echo " stop Stop all containers" + echo " clean Remove containers and images" + echo " logs Show container logs" + echo " help Show this help message" + echo "" +} + +# Build Docker image +build_image() { + log_info "Building apskel-pos-backend Docker image..." + + # Build the image with production target + docker build \ + --target production \ + -t apskel-pos-backend:latest \ + -t apskel-pos-backend:$(date +%Y%m%d-%H%M%S) \ + . + + if [ $? -eq 0 ]; then + log_success "Docker image built successfully!" + else + log_error "Failed to build Docker image" + exit 1 + fi +} + +# Run production container +run_container() { + log_info "Starting production containers..." + + # Start the containers + docker-compose up -d + + if [ $? -eq 0 ]; then + log_success "Containers started successfully!" + log_info "Backend API available at: http://localhost:3300" + log_info "Database available at: localhost:5432" + log_info "Redis available at: localhost:6379" + log_info "" + log_info "Use 'docker-compose logs -f backend' to view logs" + else + log_error "Failed to start containers" + exit 1 + fi +} + +# Run development environment +run_dev() { + log_info "Starting development environment..." + + # Start development containers + docker-compose --profile dev up -d + + if [ $? -eq 0 ]; then + log_success "Development environment started!" + log_info "Backend API (dev) available at: http://localhost:3001" + log_info "Backend API (prod) available at: http://localhost:3300" + log_info "" + log_info "Use 'docker-compose logs -f backend-dev' to view development logs" + else + log_error "Failed to start development environment" + exit 1 + fi +} + +# Run migrations +run_migrations() { + log_info "Running database migrations..." + + # Ensure database is running + docker-compose up -d postgres + sleep 5 + + # Run migrations + docker-compose --profile migrate up migrate + + if [ $? -eq 0 ]; then + log_success "Migrations completed successfully!" + else + log_warning "Migrations may have failed or are already up to date" + fi +} + +# Stop containers +stop_containers() { + log_info "Stopping all containers..." + + docker-compose down + + if [ $? -eq 0 ]; then + log_success "All containers stopped" + else + log_error "Failed to stop containers" + exit 1 + fi +} + +# Clean up containers and images +clean_up() { + log_warning "This will remove all containers, networks, and images related to this project" + read -p "Are you sure? (y/N): " -n 1 -r + echo + + if [[ $REPLY =~ ^[Yy]$ ]]; then + log_info "Cleaning up containers and images..." + + # Stop and remove containers + docker-compose down -v --remove-orphans + + # Remove images + docker rmi apskel-pos-backend:latest || true + docker rmi $(docker images apskel-pos-backend -q) || true + + # Remove unused networks and volumes + docker network prune -f || true + docker volume prune -f || true + + log_success "Cleanup completed" + else + log_info "Cleanup cancelled" + fi +} + +# Show logs +show_logs() { + log_info "Showing container logs..." + + # Show logs for all services + docker-compose logs -f +} + +# Main script logic +case "${1:-help}" in + "build") + build_image + ;; + "run") + build_image + run_container + ;; + "dev") + run_dev + ;; + "migrate") + run_migrations + ;; + "stop") + stop_containers + ;; + "clean") + clean_up + ;; + "logs") + show_logs + ;; + "help"|*) + show_help + ;; +esac \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index bc7bb00..ba5de2c 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,8 +1,126 @@ -version: "3.3" +version: '3.8' + services: - app: - build: . - ports: - - "3300:3300" + # PostgreSQL Database + postgres: + image: postgres:15-alpine + container_name: apskel-pos-postgres + restart: unless-stopped + environment: + POSTGRES_DB: apskel_pos + POSTGRES_USER: apskel + POSTGRES_PASSWORD: 7a8UJbM2GgBWaseh0lnP3O5i1i5nINXk + POSTGRES_INITDB_ARGS: "--encoding=UTF-8" volumes: - - ./:/app/ \ No newline at end of file + - postgres_data:/var/lib/postgresql/data + - ./init.sql:/docker-entrypoint-initdb.d/init.sql + ports: + - "5432:5432" + networks: + - apskel-network + healthcheck: + test: ["CMD-SHELL", "pg_isready -U apskel -d apskel_pos"] + interval: 30s + timeout: 10s + retries: 3 + + # Redis (Optional - for caching and sessions) + redis: + image: redis:7-alpine + container_name: apskel-pos-redis + restart: unless-stopped + ports: + - "6379:6379" + volumes: + - redis_data:/data + networks: + - apskel-network + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 30s + timeout: 10s + retries: 3 + + # Backend API + backend: + build: + context: . + dockerfile: Dockerfile + target: production + container_name: apskel-pos-backend + restart: unless-stopped + environment: + ENV_MODE: production + GIN_MODE: release + ports: + - "3300:3300" + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + networks: + - apskel-network + volumes: + - ./infra:/app/infra:ro + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:3300/health || exit 1"] + interval: 30s + timeout: 10s + retries: 3 + + # Development backend (for local development) + backend-dev: + build: + context: . + dockerfile: Dockerfile + target: development + container_name: apskel-pos-backend-dev + restart: unless-stopped + environment: + ENV_MODE: development + GIN_MODE: debug + ports: + - "3001:3300" + depends_on: + postgres: + condition: service_healthy + networks: + - apskel-network + volumes: + - .:/app + - go_modules:/go/pkg/mod + profiles: + - dev + + # Migration service (run once) + migrate: + build: + context: . + dockerfile: Dockerfile + target: migration + container_name: apskel-pos-migrate + environment: + ENV_MODE: production + depends_on: + postgres: + condition: service_healthy + networks: + - apskel-network + volumes: + - ./infra:/app/infra:ro + - ./migrations:/app/migrations:ro + profiles: + - migrate + +volumes: + postgres_data: + driver: local + redis_data: + driver: local + go_modules: + driver: local + +networks: + apskel-network: + driver: bridge \ No newline at end of file diff --git a/docs/ADVANCED_ORDER_MANAGEMENT.md b/docs/ADVANCED_ORDER_MANAGEMENT.md deleted file mode 100644 index 8f10ce5..0000000 --- a/docs/ADVANCED_ORDER_MANAGEMENT.md +++ /dev/null @@ -1,463 +0,0 @@ -# Advanced Order Management API Documentation - -## Overview - -The Advanced Order Management API provides comprehensive functionality for managing orders beyond basic operations. This includes partial refunds, void operations, and bill splitting capabilities. - -## Features - -- ✅ **Partial Refund**: Refund specific items from paid orders -- ✅ **Void Order**: Cancel ongoing orders (per item or entire order) -- ✅ **Split Bill**: Split orders by items or amounts -- ✅ **Order Status Management**: Support for PARTIAL and VOIDED statuses -- ✅ **Transaction Tracking**: Complete audit trail for all operations -- ✅ **Validation**: Comprehensive validation for all operations - -## API Endpoints - -### 1. Partial Refund - -**POST** `/order/partial-refund` - -Refund specific items from a paid order while keeping the remaining items. - -#### Request Body - -```json -{ - "order_id": 123, - "reason": "Customer returned damaged items", - "items": [ - { - "order_item_id": 456, - "quantity": 2 - }, - { - "order_item_id": 789, - "quantity": 1 - } - ] -} -``` - -#### Request Parameters - -| Parameter | Type | Required | Description | -|-----------|--------|----------|--------------------------------| -| order_id | int64 | Yes | ID of the order to refund | -| reason | string | Yes | Reason for the partial refund | -| items | array | Yes | Array of items to refund | - -#### Item Parameters - -| Parameter | Type | Required | Description | -|---------------|--------|----------|--------------------------------| -| order_item_id | int64 | Yes | ID of the order item to refund | -| quantity | int | Yes | Quantity to refund (min: 1) | - -#### Response - -**Success (200 OK)** - -```json -{ - "success": true, - "status": 200, - "data": { - "order_id": 123, - "status": "PARTIAL", - "refunded_amount": 75000, - "remaining_amount": 25000, - "reason": "Customer returned damaged items", - "refunded_at": "2024-01-15T10:30:00Z", - "customer_name": "John Doe", - "payment_type": "CASH", - "refunded_items": [ - { - "order_item_id": 456, - "item_name": "Bakso Special", - "quantity": 2, - "unit_price": 25000, - "total_price": 50000 - }, - { - "order_item_id": 789, - "item_name": "Es Teh Manis", - "quantity": 1, - "unit_price": 25000, - "total_price": 25000 - } - ] - } -} -``` - -### 2. Void Order - -**POST** `/order/void` - -Void an ongoing order (NEW or PENDING status) either entirely or by specific items. - -#### Request Body - -**Void Entire Order:** -```json -{ - "order_id": 123, - "reason": "Customer cancelled order", - "type": "ALL" -} -``` - -**Void Specific Items:** -```json -{ - "order_id": 123, - "reason": "Customer changed mind about some items", - "type": "ITEM", - "items": [ - { - "order_item_id": 456, - "quantity": 1 - } - ] -} -``` - -#### Request Parameters - -| Parameter | Type | Required | Description | -|-----------|--------|----------|--------------------------------| -| order_id | int64 | Yes | ID of the order to void | -| reason | string | Yes | Reason for voiding | -| type | string | Yes | Type: "ALL" or "ITEM" | -| items | array | No | Required if type is "ITEM" | - -#### Response - -**Success (200 OK)** - -```json -{ - "success": true, - "status": 200, - "data": { - "order_id": 123, - "status": "VOIDED", - "reason": "Customer cancelled order", - "voided_at": "2024-01-15T10:30:00Z", - "customer_name": "John Doe", - "voided_items": [ - { - "order_item_id": 456, - "item_name": "Bakso Special", - "quantity": 1, - "unit_price": 25000, - "total_price": 25000 - } - ] - } -} -``` - -### 3. Split Bill - -**POST** `/order/split-bill` - -Split an order into a separate order by items or amounts. - -#### Request Body - -**Split by Items:** -```json -{ - "order_id": 123, - "type": "ITEM", - "payment_method": "CASH", - "payment_provider": "CASH", - "items": [ - { - "order_item_id": 789, - "quantity": 2 - }, - { - "order_item_id": 101, - "quantity": 1 - } - ] -} -``` - -**Split by Amount:** -```json -{ - "order_id": 123, - "type": "AMOUNT", - "payment_method": "CASH", - "payment_provider": "CASH", - "amount": 50000 -} -``` - -#### Request Parameters - -| Parameter | Type | Required | Description | -|------------------|--------|----------|--------------------------------| -| order_id | int64 | Yes | ID of the order to split | -| type | string | Yes | Type: "ITEM" or "AMOUNT" | -| payment_method | string | Yes | Payment method for split order | -| payment_provider | string | No | Payment provider for split order| -| items | array | No | Required if type is "ITEM" | -| amount | float | No | Required if type is "AMOUNT" (must be less than order total) | - -#### Item Parameters - -| Parameter | Type | Required | Description | -|---------------|--------|----------|--------------------------------| -| order_item_id | int64 | Yes | ID of the order item to split | -| quantity | int | Yes | Quantity to split (min: 1) | - -#### Response - -**Success (200 OK)** - -```json -{ - "success": true, - "status": 200, - "data": { - "id": 124, - "partner_id": 1, - "status": "PAID", - "amount": 100000, - "total": 110000, - "tax": 10000, - "customer_id": 456, - "customer_name": "John Doe", - "payment_type": "CASH", - "payment_provider": "CASH", - "source": "POS", - "created_at": "2024-01-15T10:30:00Z", - "updated_at": "2024-01-15T10:30:00Z", - "order_items": [ - { - "id": 789, - "item_id": 1, - "item_name": "Bakso Special", - "price": 50000, - "quantity": 2, - "subtotal": 100000 - } - ] - } -} -``` - -## Business Logic - -### Partial Refund Process - -1. **Validation** - - Verify order exists and belongs to partner - - Ensure order status is "PAID" - - Validate refund items exist and quantities are valid - -2. **Item Updates** - - Reduce quantities of refunded items - - Remove items completely if quantity becomes 0 - - Recalculate order totals - -3. **Order Status Update** - - Set status to "PARTIAL" if items remain - - Set status to "REFUNDED" if all items refunded - -4. **Transaction Creation** - - Create refund transaction with negative amount - - Track refund details - -### Void Order Process - -1. **Validation** - - Verify order exists and belongs to partner - - Ensure order status is "NEW" or "PENDING" - - Validate void items if type is "ITEM" - -2. **Void Operations** - - **ALL**: Set order status to "VOIDED" - - **ITEM**: Reduce quantities and recalculate totals - -3. **Status Management** - - Set status to "PARTIAL" if items remain - - Set status to "VOIDED" if all items voided - -### Split Bill Process - -1. **Validation** - - Verify order exists and belongs to partner - - Ensure order status is "NEW" or "PENDING" - - Validate split configuration - -2. **Split Operations** - - **ITEM**: Create new PAID order with specified items, reduce quantities in original order - - **AMOUNT**: Create new PAID order with specified amount, reduce amount in original order - -3. **Order Management** - - Original order remains PENDING with reduced items/amount - - New split order becomes PAID with specified payment method - - Recalculate totals for both orders - -## Order Status Flow - -``` -NEW → PENDING → PAID → REFUNDED - ↓ ↓ ↓ -VOIDED VOIDED PARTIAL -``` - -## Error Handling - -### Common Error Responses - -**Order Not Found (404)** -```json -{ - "success": false, - "status": 404, - "message": "order not found" -} -``` - -**Invalid Order Status (400)** -```json -{ - "success": false, - "status": 400, - "message": "only paid order can be partially refunded" -} -``` - -**Invalid Quantity (400)** -```json -{ - "success": false, - "status": 400, - "message": "refund quantity 3 exceeds available quantity 2 for item 456" -} -``` - -**Split Amount Mismatch (400)** -```json -{ - "success": false, - "status": 400, - "message": "split amount 95000 must be less than order total 100000" -} -``` - -## Database Schema Updates - -### Orders Table - -```sql --- New statuses supported -ALTER TABLE orders ADD CONSTRAINT check_status -CHECK (status IN ('NEW', 'PENDING', 'PAID', 'REFUNDED', 'VOIDED', 'PARTIAL')); -``` - -### Order Items Table - -```sql --- Support for quantity updates -ALTER TABLE order_items ADD COLUMN updated_at TIMESTAMP DEFAULT NOW(); -``` - -## Constants - -### Order Status - -```go -const ( - New OrderStatus = "NEW" - Paid OrderStatus = "PAID" - Cancel OrderStatus = "CANCEL" - Pending OrderStatus = "PENDING" - Refunded OrderStatus = "REFUNDED" - Voided OrderStatus = "VOIDED" // New - Partial OrderStatus = "PARTIAL" // New -) -``` - -## Testing Examples - -### cURL Examples - -**Partial Refund:** -```bash -curl -X POST http://localhost:8080/api/v1/order/partial-refund \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer YOUR_JWT_TOKEN" \ - -d '{ - "order_id": 123, - "reason": "Customer returned damaged items", - "items": [ - { - "order_item_id": 456, - "quantity": 2 - } - ] - }' -``` - -**Void Order:** -```bash -curl -X POST http://localhost:8080/api/v1/order/void \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer YOUR_JWT_TOKEN" \ - -d '{ - "order_id": 123, - "reason": "Customer cancelled order", - "type": "ALL" - }' -``` - -**Split Bill:** -```bash -curl -X POST http://localhost:8080/api/v1/order/split-bill \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer YOUR_JWT_TOKEN" \ - -d '{ - "order_id": 123, - "type": "ITEM", - "payment_method": "CASH", - "payment_provider": "CASH", - "items": [ - { - "order_item_id": 456, - "quantity": 1 - }, - { - "order_item_id": 789, - "quantity": 1 - } - ] - }' -``` - -## Security Considerations - -1. **Authorization**: Only authorized users can perform these operations -2. **Audit Trail**: All operations are logged with user and timestamp -3. **Validation**: Strict validation prevents invalid operations -4. **Data Integrity**: Transaction-based operations ensure consistency - -## Future Enhancements - -1. **Bulk Operations**: Support for bulk partial refunds/voids -2. **Approval Workflow**: Multi-level approval for large operations -3. **Notification System**: Customer notifications for refunds/voids -4. **Analytics**: Dashboard for operation trends and analysis -5. **Integration**: Integration with inventory management systems - -## Support - -For questions or issues with the Advanced Order Management API, please contact the development team or create an issue in the project repository. \ No newline at end of file diff --git a/docs/IMPLEMENTATION_SUMMARY.md b/docs/IMPLEMENTATION_SUMMARY.md deleted file mode 100644 index 6ae3fe3..0000000 --- a/docs/IMPLEMENTATION_SUMMARY.md +++ /dev/null @@ -1,297 +0,0 @@ -# Advanced Order Management Implementation Summary - -## Overview - -This document summarizes the complete implementation of advanced order management features for the Enaklo POS backend system. The implementation includes three major features: **Partial Refund**, **Void Order**, and **Split Bill** functionality. - -## 🎯 Implemented Features - -### 1. Partial Refund System -**Purpose**: Allow refunding specific items from paid orders while keeping remaining items. - -**Key Components**: -- ✅ **API Endpoint**: `POST /order/partial-refund` -- ✅ **Service Method**: `PartialRefundRequest()` -- ✅ **Repository Methods**: `UpdateOrderItem()`, `UpdateOrderTotals()` -- ✅ **Validation**: Order status, item existence, quantity validation -- ✅ **Transaction Tracking**: Creates refund transactions with negative amounts -- ✅ **Status Management**: Updates order to "PARTIAL" or "REFUNDED" - -**Business Logic**: -```go -// Flow: PAID → PARTIAL/REFUNDED -// - Validate order is PAID -// - Reduce item quantities -// - Recalculate totals -// - Create refund transaction -// - Update order status -``` - -### 2. Void Order System -**Purpose**: Cancel ongoing orders (NEW/PENDING) either entirely or by specific items. - -**Key Components**: -- ✅ **API Endpoint**: `POST /order/void` -- ✅ **Service Method**: `VoidOrderRequest()` -- ✅ **Two Modes**: "ALL" (entire order) or "ITEM" (specific items) -- ✅ **Validation**: Order status, item existence, quantity validation -- ✅ **Status Management**: Updates order to "VOIDED" or "PARTIAL" - -**Business Logic**: -```go -// Flow: NEW/PENDING → VOIDED/PARTIAL -// - Validate order is NEW or PENDING -// - ALL: Set status to VOIDED -// - ITEM: Reduce quantities, recalculate totals -// - Update order status accordingly -``` - -### 3. Split Bill System -**Purpose**: Split orders into a separate order by items or amounts. - -**Key Components**: -- ✅ **API Endpoint**: `POST /order/split-bill` -- ✅ **Service Method**: `SplitBillRequest()` -- ✅ **Two Modes**: "ITEM" (specify items) or "AMOUNT" (specify amount) -- ✅ **Order Creation**: Creates a new order for the split -- ✅ **Original Order**: Voids the original order after splitting - -**Business Logic**: -```go -// Flow: NEW/PENDING → PENDING (reduced) + PAID (split) -// - Validate order is NEW or PENDING -// - ITEM: Create PAID order with specified items, reduce quantities in original -// - AMOUNT: Create PAID order with specified amount, reduce amount in original -// - Original order remains PENDING with reduced items/amount -// - New split order becomes PAID with specified payment method -``` - -## 🏗️ Architecture Components - -### 1. Constants & Status Management -```go -// Added new order statuses -const ( - New OrderStatus = "NEW" - Paid OrderStatus = "PAID" - Cancel OrderStatus = "CANCEL" - Pending OrderStatus = "PENDING" - Refunded OrderStatus = "REFUNDED" - Voided OrderStatus = "VOIDED" // New - Partial OrderStatus = "PARTIAL" // New -) -``` - -### 2. Entity Models -```go -// New entity types for request/response handling -type PartialRefundItem struct { - OrderItemID int64 `json:"order_item_id" validate:"required"` - Quantity int `json:"quantity" validate:"required,min=1"` -} - -type VoidItem struct { - OrderItemID int64 `json:"order_item_id" validate:"required"` - Quantity int `json:"quantity" validate:"required,min=1"` -} - -type SplitBillSplit struct { - CustomerName string `json:"customer_name" validate:"required"` - CustomerID *int64 `json:"customer_id"` - Items []SplitBillItem `json:"items,omitempty"` - Amount float64 `json:"amount,omitempty"` -} -``` - -### 3. Repository Layer -```go -// New repository methods -type Repository interface { - // ... existing methods - UpdateOrderItem(ctx mycontext.Context, orderItemID int64, quantity int) error - UpdateOrderTotals(ctx mycontext.Context, orderID int64, amount, tax, total float64) error -} -``` - -### 4. Service Layer -```go -// New service methods -type Service interface { - // ... existing methods - PartialRefundRequest(ctx mycontext.Context, partnerID, orderID int64, reason string, items []entity.PartialRefundItem) error - VoidOrderRequest(ctx mycontext.Context, partnerID, orderID int64, reason string, voidType string, items []entity.VoidItem) error - SplitBillRequest(ctx mycontext.Context, partnerID, orderID int64, splitType string, splits []entity.SplitBillSplit) ([]*entity.Order, error) -} -``` - -### 5. HTTP Handlers -```go -// New API endpoints -func (h *Handler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) { - // ... existing routes - route.POST("/partial-refund", jwt, h.PartialRefund) - route.POST("/void", jwt, h.VoidOrder) - route.POST("/split-bill", jwt, h.SplitBill) -} -``` - -## 📊 Order Status Flow - -``` -NEW → PENDING → PAID → REFUNDED - ↓ ↓ ↓ -VOIDED VOIDED PARTIAL -``` - -**Status Transitions**: -- **NEW/PENDING** → **VOIDED**: When entire order is voided -- **NEW/PENDING** → **PARTIAL**: When some items are voided -- **PAID** → **PARTIAL**: When some items are refunded -- **PAID** → **REFUNDED**: When all items are refunded - -## 🔒 Validation & Security - -### Input Validation -- ✅ **Order Existence**: Verify order exists and belongs to partner -- ✅ **Status Validation**: Ensure appropriate status for operations -- ✅ **Item Validation**: Verify items exist and quantities are valid -- ✅ **Quantity Validation**: Prevent refunding/voiding more than available -- ✅ **Split Validation**: Ensure split amounts match order total - -### Business Rules -- ✅ **Partial Refund**: Only PAID orders can be partially refunded -- ✅ **Void Order**: Only NEW/PENDING orders can be voided -- ✅ **Split Bill**: Only NEW/PENDING orders can be split -- ✅ **Transaction Tracking**: All operations create audit trails - -## 🧪 Testing - -### Test Coverage -- ✅ **Unit Tests**: Comprehensive test coverage for all service methods -- ✅ **Mock Testing**: Uses testify/mock for dependency mocking -- ✅ **Edge Cases**: Tests for invalid states and error conditions -- ✅ **Success Scenarios**: Tests for successful operations - -### Test Files -- `internal/services/v2/order/refund_test.go` - Original refund tests -- `internal/services/v2/order/advanced_order_management_test.go` - New feature tests - -## 📚 Documentation - -### API Documentation -- ✅ **REFUND_API.md**: Complete refund API documentation -- ✅ **ADVANCED_ORDER_MANAGEMENT.md**: Comprehensive feature documentation -- ✅ **IMPLEMENTATION_SUMMARY.md**: This summary document - -### Documentation Features -- ✅ **Request/Response Examples**: Complete JSON examples -- ✅ **Error Handling**: Common error scenarios and responses -- ✅ **Business Logic**: Detailed process flows -- ✅ **cURL Examples**: Ready-to-use API testing commands - -## 🚀 Usage Examples - -### Partial Refund -```bash -curl -X POST /order/partial-refund \ - -H "Authorization: Bearer TOKEN" \ - -d '{ - "order_id": 123, - "reason": "Customer returned damaged items", - "items": [ - {"order_item_id": 456, "quantity": 2} - ] - }' -``` - -### Void Order -```bash -curl -X POST /order/void \ - -H "Authorization: Bearer TOKEN" \ - -d '{ - "order_id": 123, - "reason": "Customer cancelled order", - "type": "ALL" - }' -``` - -### Split Bill -```bash -curl -X POST /order/split-bill \ - -H "Authorization: Bearer TOKEN" \ - -d '{ - "order_id": 123, - "type": "ITEM", - "payment_method": "CASH", - "payment_provider": "CASH", - "items": [ - {"order_item_id": 456, "quantity": 1}, - {"order_item_id": 789, "quantity": 1} - ] - }' -``` - -## 🔧 Database Considerations - -### Schema Updates -```sql --- New statuses supported -ALTER TABLE orders ADD CONSTRAINT check_status -CHECK (status IN ('NEW', 'PENDING', 'PAID', 'REFUNDED', 'VOIDED', 'PARTIAL')); - --- Support for quantity updates -ALTER TABLE order_items ADD COLUMN updated_at TIMESTAMP DEFAULT NOW(); -``` - -### Transaction Management -- ✅ **Atomic Operations**: All operations use database transactions -- ✅ **Rollback Support**: Failed operations are properly rolled back -- ✅ **Data Consistency**: Ensures order totals match item totals - -## 🎯 Benefits - -### Business Benefits -1. **Flexibility**: Support for complex order management scenarios -2. **Customer Satisfaction**: Handle partial returns and cancellations -3. **Operational Efficiency**: Streamlined bill splitting for groups -4. **Audit Trail**: Complete tracking of all order modifications - -### Technical Benefits -1. **Scalable Architecture**: Clean separation of concerns -2. **Comprehensive Testing**: High test coverage ensures reliability -3. **Extensible Design**: Easy to add new order management features -4. **Documentation**: Complete API documentation for integration - -## 🔮 Future Enhancements - -### Potential Improvements -1. **Bulk Operations**: Support for bulk partial refunds/voids -2. **Approval Workflow**: Multi-level approval for large operations -3. **Notification System**: Customer notifications for refunds/voids -4. **Analytics Dashboard**: Order management trends and analysis -5. **Inventory Integration**: Automatic inventory updates for refunds/voids - -### Integration Opportunities -1. **Payment Gateway**: Direct refund processing -2. **Customer Management**: Customer point adjustments -3. **Reporting System**: Enhanced order analytics -4. **Mobile App**: Real-time order management - -## 📋 Implementation Checklist - -- ✅ **Core Features**: All three main features implemented -- ✅ **API Endpoints**: Complete REST API implementation -- ✅ **Service Layer**: Business logic implementation -- ✅ **Repository Layer**: Database operations -- ✅ **Validation**: Comprehensive input validation -- ✅ **Error Handling**: Proper error responses -- ✅ **Testing**: Unit test coverage -- ✅ **Documentation**: Complete API documentation -- ✅ **Status Management**: New order statuses -- ✅ **Transaction Tracking**: Audit trail implementation - -## 🎉 Conclusion - -The Advanced Order Management system provides a comprehensive solution for complex order scenarios in the Enaklo POS system. The implementation follows best practices for scalability, maintainability, and reliability, with complete documentation and testing coverage. - -The system is now ready for production use and provides the foundation for future enhancements and integrations. \ No newline at end of file diff --git a/docs/REFUND_API.md b/docs/REFUND_API.md deleted file mode 100644 index 43ec180..0000000 --- a/docs/REFUND_API.md +++ /dev/null @@ -1,271 +0,0 @@ -# Refund Order API Documentation - -## Overview - -The Refund Order API provides comprehensive functionality to process refunds for paid orders. This includes order status updates, transaction creation, customer voucher reversal, payment gateway refunds, and customer notifications. - -## Features - -- ✅ **Order Status Management**: Updates order status to "REFUNDED" -- ✅ **Transaction Tracking**: Creates refund transactions with negative amounts -- ✅ **Customer Voucher Reversal**: Reverses any vouchers/points given for the order -- ✅ **Payment Gateway Integration**: Handles refunds for non-cash payments -- ✅ **Customer Notifications**: Sends email notifications for refunds -- ✅ **Audit Trail**: Tracks who processed the refund and when -- ✅ **Refund History**: Provides endpoint to view refund history - -## API Endpoints - -### 1. Process Refund - -**POST** `/order/refund` - -Process a refund for a paid order. - -#### Request Body - -```json -{ - "order_id": 123, - "reason": "Customer request" -} -``` - -#### Request Parameters - -| Parameter | Type | Required | Description | -|-----------|--------|----------|--------------------------------| -| order_id | int64 | Yes | ID of the order to refund | -| reason | string | Yes | Reason for the refund | - -#### Response - -**Success (200 OK)** - -```json -{ - "success": true, - "status": 200, - "data": { - "order_id": 123, - "status": "REFUNDED", - "refund_amount": 100000, - "reason": "Customer request", - "refunded_at": "2024-01-15T10:30:00Z", - "customer_name": "John Doe", - "payment_type": "CASH" - } -} -``` - -**Error (400 Bad Request)** - -```json -{ - "success": false, - "status": 400, - "message": "only paid order can be refund" -} -``` - -### 2. Get Refund History - -**GET** `/order/refund-history` - -Retrieve refund history with filtering and pagination. - -#### Query Parameters - -| Parameter | Type | Required | Description | -|-------------|--------|----------|--------------------------------| -| limit | int | No | Number of records (max 100) | -| offset | int | No | Number of records to skip | -| start_date | string | No | Start date (RFC3339 format) | -| end_date | string | No | End date (RFC3339 format) | - -#### Response - -**Success (200 OK)** - -```json -{ - "success": true, - "status": 200, - "data": [ - { - "order_id": 123, - "customer_name": "John Doe", - "customer_id": 456, - "is_member": true, - "status": "REFUNDED", - "amount": 95000, - "total": 100000, - "payment_type": "CASH", - "table_number": "A1", - "order_type": "DINE_IN", - "created_at": "2024-01-15T09:00:00Z", - "refunded_at": "2024-01-15T10:30:00Z", - "tax": 5000 - } - ], - "paging_meta": { - "page": 1, - "total": 25, - "limit": 20 - } -} -``` - -## Business Logic - -### Refund Process Flow - -1. **Validation** - - Verify order exists and belongs to partner - - Ensure order status is "PAID" - - Validate refund reason - -2. **Order Update** - - Update order status to "REFUNDED" - - Store refund reason in order description - - Update timestamp - -3. **Transaction Creation** - - Create refund transaction with negative amount - - Set transaction type to "REFUND" - - Track who processed the refund - -4. **Customer Voucher Reversal** - - Find vouchers associated with the order - - Mark vouchers as reversed/cancelled - - Adjust customer points if applicable - -5. **Payment Gateway Refund** - - For non-cash payments, call payment gateway refund API - - Handle gateway response and errors - - Update transaction with gateway details - -6. **Customer Notification** - - Send email notification to customer - - Include refund details and reason - - Provide transaction reference - -### Supported Payment Methods - -| Payment Method | Refund Handling | -|----------------|-----------------------------------| -| CASH | Manual refund (no gateway call) | -| QRIS | Gateway refund via provider API | -| CARD | Gateway refund via provider API | -| TRANSFER | Gateway refund via provider API | -| ONLINE | Gateway refund via provider API | - -### Error Handling - -- **Order not found**: Returns 404 error -- **Order not paid**: Returns 400 error with message -- **Voucher reversal failure**: Logs warning but continues refund -- **Payment gateway failure**: Logs error but continues refund -- **Notification failure**: Logs warning but continues refund - -## Database Schema - -### Orders Table - -```sql -ALTER TABLE orders ADD COLUMN description TEXT; -``` - -### Transactions Table - -```sql --- Refund transactions have negative amounts --- Transaction type: "REFUND" --- Status: "REFUND" -``` - -## Constants - -### Order Status - -```go -const ( - New OrderStatus = "NEW" - Paid OrderStatus = "PAID" - Cancel OrderStatus = "CANCEL" - Pending OrderStatus = "PENDING" - Refunded OrderStatus = "REFUNDED" // New status -) -``` - -### Transaction Status - -```go -const ( - New PaymentStatus = "NEW" - Paid PaymentStatus = "PAID" - Cancel PaymentStatus = "CANCEL" - Refund PaymentStatus = "REFUND" // New status -) -``` - -## Testing - -Run the refund tests: - -```bash -go test ./internal/services/v2/order -v -run TestRefund -``` - -## Security Considerations - -1. **Authorization**: Only authorized users can process refunds -2. **Audit Trail**: All refunds are logged with user and timestamp -3. **Validation**: Strict validation prevents invalid refunds -4. **Rate Limiting**: Consider implementing rate limiting for refund endpoints - -## Future Enhancements - -1. **Partial Refunds**: Support for refunding specific order items -2. **Refund Approval Workflow**: Multi-level approval for large refunds -3. **Refund Analytics**: Dashboard for refund trends and analysis -4. **Automated Refunds**: Integration with customer service systems -5. **Refund Templates**: Predefined refund reasons and templates - -## Integration Examples - -### cURL Example - -```bash -curl -X POST http://localhost:8080/api/v1/order/refund \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer YOUR_JWT_TOKEN" \ - -d '{ - "order_id": 123, - "reason": "Customer request" - }' -``` - -### JavaScript Example - -```javascript -const refundOrder = async (orderId, reason) => { - const response = await fetch('/api/v1/order/refund', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${token}` - }, - body: JSON.stringify({ - order_id: orderId, - reason: reason - }) - }); - - return response.json(); -}; -``` - -## Support - -For questions or issues with the refund API, please contact the development team or create an issue in the project repository. \ No newline at end of file diff --git a/docs/docs.go b/docs/docs.go deleted file mode 100644 index c187a9f..0000000 --- a/docs/docs.go +++ /dev/null @@ -1,3101 +0,0 @@ -// Package docs Code generated by swaggo/swag. DO NOT EDIT -package docs - -import "github.com/swaggo/swag" - -const docTemplate = `{ - "schemes": {{ marshal .Schemes }}, - "swagger": "2.0", - "info": { - "description": "{{escape .Description}}", - "title": "{{.Title}}", - "contact": {}, - "version": "{{.Version}}" - }, - "host": "{{.Host}}", - "basePath": "{{.BasePath}}", - "paths": { - "/api/v1/auth/login": { - "post": { - "description": "Authenticates a user based on the provided credentials and returns a JWT token.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Auth Login API's" - ], - "summary": "User login", - "parameters": [ - { - "description": "User login credentials", - "name": "bodyParam", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.LoginRequest" - } - } - ], - "responses": { - "200": { - "description": "Login successful", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/response.LoginResponse" - } - } - } - ] - } - }, - "400": { - "description": "Bad request", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - } - } - } - }, - "/api/v1/branch": { - "post": { - "description": "Create a new branch based on the provided data.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Branch APIs" - ], - "summary": "Create a new branch", - "parameters": [ - { - "type": "string", - "description": "JWT token", - "name": "Authorization", - "in": "header", - "required": true - }, - { - "description": "New branch details", - "name": "req", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.Branch" - } - } - ], - "responses": { - "200": { - "description": "Branch created successfully", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/response.Branch" - } - } - } - ] - } - }, - "400": { - "description": "Bad request", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - } - } - } - }, - "/api/v1/branch/list": { - "get": { - "description": "Get a paginated list of branches based on query parameters.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Branch APIs" - ], - "summary": "Get a list of branches", - "parameters": [ - { - "type": "string", - "description": "JWT token", - "name": "Authorization", - "in": "header", - "required": true - }, - { - "type": "integer", - "description": "Number of items to retrieve (default 10)", - "name": "Limit", - "in": "query" - }, - { - "type": "integer", - "description": "Offset for pagination (default 0)", - "name": "Offset", - "in": "query" - } - ], - "responses": { - "200": { - "description": "List of branches", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/response.BranchList" - } - } - } - ] - } - }, - "400": { - "description": "Bad request", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - } - } - } - }, - "/api/v1/branch/{id}": { - "get": { - "description": "Get details of a branch based on the provided ID.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Branch APIs" - ], - "summary": "Get details of a branch by ID", - "parameters": [ - { - "type": "string", - "description": "JWT token", - "name": "Authorization", - "in": "header", - "required": true - }, - { - "type": "integer", - "description": "Branch ID to retrieve", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "Branch details", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/response.Branch" - } - } - } - ] - } - }, - "400": { - "description": "Bad request", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - } - } - }, - "put": { - "description": "Update the details of an existing branch based on the provided ID.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Branch APIs" - ], - "summary": "Update an existing branch", - "parameters": [ - { - "type": "string", - "description": "JWT token", - "name": "Authorization", - "in": "header", - "required": true - }, - { - "type": "integer", - "description": "Branch ID to update", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "Updated branch details", - "name": "req", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.Branch" - } - } - ], - "responses": { - "200": { - "description": "Branch updated successfully", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/response.Branch" - } - } - } - ] - } - }, - "400": { - "description": "Bad request", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - } - } - }, - "delete": { - "description": "Delete a branch based on the provided ID.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Branch APIs" - ], - "summary": "Delete a branch by ID", - "parameters": [ - { - "type": "string", - "description": "JWT token", - "name": "Authorization", - "in": "header", - "required": true - }, - { - "type": "integer", - "description": "Branch ID to delete", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "Branch deleted successfully", - "schema": { - "$ref": "#/definitions/response.BaseResponse" - } - }, - "400": { - "description": "Bad request", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - } - } - } - }, - "/api/v1/file/upload": { - "post": { - "description": "Upload a file to Alibaba Cloud OSS with the provided details.", - "consumes": [ - "multipart/form-data" - ], - "produces": [ - "application/json" - ], - "tags": [ - "File Upload API" - ], - "summary": "Upload a file to OSS", - "parameters": [ - { - "type": "string", - "description": "JWT token", - "name": "Authorization", - "in": "header", - "required": true - }, - { - "type": "file", - "description": "File to upload (max size: 2MB)", - "name": "file", - "in": "formData", - "required": true - } - ], - "responses": { - "200": { - "description": "File uploaded successfully", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/entity.UploadFileResponse" - } - } - } - ] - } - }, - "400": { - "description": "Bad request", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - } - } - } - }, - "/api/v1/order": { - "post": { - "description": "Create a new order with the provided details.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "summary": "Create a new order", - "parameters": [ - { - "type": "string", - "description": "JWT token", - "name": "Authorization", - "in": "header", - "required": true - }, - { - "description": "Order details", - "name": "order", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.Order" - } - } - ], - "responses": { - "200": { - "description": "Order created successfully", - "schema": { - "$ref": "#/definitions/response.BaseResponse" - } - }, - "400": { - "description": "Bad request", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - } - } - } - }, - "/api/v1/order/branch-revenue": { - "get": { - "description": "Retrieve the branch-wise revenue for orders based on the specified parameters.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "summary": "Get branch-wise revenue for orders", - "parameters": [ - { - "type": "string", - "description": "JWT token", - "name": "Authorization", - "in": "header", - "required": true - }, - { - "type": "integer", - "description": "Branch ID for filtering", - "name": "branch_id", - "in": "query" - }, - { - "type": "string", - "description": "Start date for filtering (format: 'YYYY-MM-DD')", - "name": "start_date", - "in": "query" - }, - { - "type": "string", - "description": "End date for filtering (format: 'YYYY-MM-DD')", - "name": "end_date", - "in": "query" - } - ], - "responses": { - "200": { - "description": "Branch-wise revenue retrieved successfully", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/response.OrderBranchRevenue" - } - } - } - } - ] - } - }, - "400": { - "description": "Bad request", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "500": { - "description": "Internal server error", - "schema": { - "$ref": "#/definitions/response.BaseResponse" - } - } - } - } - }, - "/api/v1/order/list": { - "get": { - "description": "Retrieve a list of orders based on the specified parameters.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "summary": "Get a list of orders", - "parameters": [ - { - "type": "string", - "description": "JWT token", - "name": "Authorization", - "in": "header", - "required": true - }, - { - "type": "integer", - "description": "Number of items to retrieve (default: 10)", - "name": "limit", - "in": "query" - }, - { - "type": "integer", - "description": "Number of items to skip (default: 0)", - "name": "offset", - "in": "query" - } - ], - "responses": { - "200": { - "description": "List of orders retrieved successfully", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/response.OrderList" - } - } - } - ] - } - }, - "400": { - "description": "Bad request", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "500": { - "description": "Internal server error", - "schema": { - "$ref": "#/definitions/response.BaseResponse" - } - } - } - } - }, - "/api/v1/order/total-revenue": { - "get": { - "description": "Retrieve the total revenue and number of transactions for orders based on the specified parameters.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "summary": "Get total revenue and number of transactions for orders", - "parameters": [ - { - "type": "string", - "description": "JWT token", - "name": "Authorization", - "in": "header", - "required": true - }, - { - "type": "string", - "description": "Start date for filtering (format: 'YYYY-MM-DD')", - "name": "start_date", - "in": "query" - }, - { - "type": "string", - "description": "End date for filtering (format: 'YYYY-MM-DD')", - "name": "end_date", - "in": "query" - } - ], - "responses": { - "200": { - "description": "Total revenue and transactions retrieved successfully", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/response.OrderMonthlyRevenue" - } - } - } - ] - } - }, - "400": { - "description": "Bad request", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "500": { - "description": "Internal server error", - "schema": { - "$ref": "#/definitions/response.BaseResponse" - } - } - } - } - }, - "/api/v1/order/update-status/{id}": { - "put": { - "description": "Update the status of the specified order with the provided details.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "summary": "Update the status of an order", - "parameters": [ - { - "type": "string", - "description": "JWT token", - "name": "Authorization", - "in": "header", - "required": true - }, - { - "type": "string", - "description": "Order ID", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "Status details", - "name": "status", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.UpdateStatus" - } - } - ], - "responses": { - "200": { - "description": "Order status updated successfully", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/response.Order" - } - } - } - ] - } - }, - "400": { - "description": "Bad request", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "500": { - "description": "Internal server error", - "schema": { - "$ref": "#/definitions/response.BaseResponse" - } - } - } - } - }, - "/api/v1/order/yearly-revenue/{year}": { - "get": { - "description": "Retrieve the yearly revenue for orders based on the specified year.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "summary": "Get yearly revenue for orders", - "parameters": [ - { - "type": "string", - "description": "JWT token", - "name": "Authorization", - "in": "header", - "required": true - }, - { - "type": "integer", - "description": "Year for filtering", - "name": "year", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "Yearly revenue retrieved successfully", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": { - "type": "object", - "additionalProperties": { - "type": "object", - "additionalProperties": { - "type": "number" - } - } - } - } - } - ] - } - }, - "400": { - "description": "Bad request", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "500": { - "description": "Internal server error", - "schema": { - "$ref": "#/definitions/response.BaseResponse" - } - } - } - } - }, - "/api/v1/order/{id}": { - "get": { - "description": "Retrieve the details of the specified order by ID.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "summary": "Get details of an order by ID", - "parameters": [ - { - "type": "string", - "description": "JWT token", - "name": "Authorization", - "in": "header", - "required": true - }, - { - "type": "string", - "description": "Order ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "Order details retrieved successfully", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/response.Order" - } - } - } - ] - } - }, - "400": { - "description": "Bad request", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "500": { - "description": "Internal server error", - "schema": { - "$ref": "#/definitions/response.BaseResponse" - } - } - } - } - }, - "/api/v1/product/": { - "post": { - "description": "Create a new product with the provided details.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Product APIs" - ], - "summary": "Create a new product", - "parameters": [ - { - "type": "string", - "description": "JWT token", - "name": "Authorization", - "in": "header", - "required": true - }, - { - "description": "Product details to create", - "name": "req", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.Product" - } - } - ], - "responses": { - "200": { - "description": "Product created successfully", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/response.Product" - } - } - } - ] - } - }, - "400": { - "description": "Bad request", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - } - } - } - }, - "/api/v1/product/list": { - "get": { - "description": "Get a paginated list of products based on query parameters.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Product APIs" - ], - "summary": "Get a list of products", - "parameters": [ - { - "type": "string", - "description": "JWT token", - "name": "Authorization", - "in": "header", - "required": true - }, - { - "type": "integer", - "description": "Number of items to retrieve (default 10)", - "name": "Limit", - "in": "query" - }, - { - "type": "integer", - "description": "Offset for pagination (default 0)", - "name": "Offset", - "in": "query" - } - ], - "responses": { - "200": { - "description": "List of products", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/response.ProductList" - } - } - } - ] - } - }, - "400": { - "description": "Bad request", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - } - } - } - }, - "/api/v1/product/{id}": { - "get": { - "description": "Get details of a product based on the provided ID.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Product APIs" - ], - "summary": "Get details of a product by ID", - "parameters": [ - { - "type": "string", - "description": "JWT token", - "name": "Authorization", - "in": "header", - "required": true - }, - { - "type": "integer", - "description": "Product ID to retrieve", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "Product details", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/response.Product" - } - } - } - ] - } - }, - "400": { - "description": "Bad request", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - } - } - }, - "put": { - "description": "Update the details of an existing product based on the provided ID.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Product APIs" - ], - "summary": "Update an existing product", - "parameters": [ - { - "type": "string", - "description": "JWT token", - "name": "Authorization", - "in": "header", - "required": true - }, - { - "type": "integer", - "description": "Product ID to update", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "Updated product details", - "name": "req", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.Product" - } - } - ], - "responses": { - "200": { - "description": "Product updated successfully", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/response.Product" - } - } - } - ] - } - }, - "400": { - "description": "Bad request", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - } - } - }, - "delete": { - "description": "Delete a product based on the provided ID.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Product APIs" - ], - "summary": "Delete a product by ID", - "parameters": [ - { - "type": "string", - "description": "JWT token", - "name": "Authorization", - "in": "header", - "required": true - }, - { - "type": "integer", - "description": "Product ID to delete", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "Product deleted successfully", - "schema": { - "$ref": "#/definitions/response.BaseResponse" - } - }, - "400": { - "description": "Bad request", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - } - } - } - }, - "/api/v1/studio": { - "post": { - "description": "Create a new studio based on the provided details.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Studio APIs" - ], - "summary": "Create a new studio", - "parameters": [ - { - "type": "string", - "description": "JWT token", - "name": "Authorization", - "in": "header", - "required": true - }, - { - "description": "New studio details", - "name": "req", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.Studio" - } - } - ], - "responses": { - "200": { - "description": "Studio created successfully", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/response.Studio" - } - } - } - ] - } - }, - "400": { - "description": "Bad request", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - } - } - } - }, - "/api/v1/studio/search": { - "get": { - "description": "Search for studios based on query parameters.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Studio APIs" - ], - "summary": "Search for studios", - "parameters": [ - { - "type": "string", - "description": "JWT token", - "name": "Authorization", - "in": "header", - "required": true - }, - { - "type": "string", - "description": "Studio name for search", - "name": "Name", - "in": "query" - }, - { - "type": "string", - "description": "Studio status for search", - "name": "Status", - "in": "query" - }, - { - "type": "integer", - "description": "Number of items to retrieve (default 10)", - "name": "Limit", - "in": "query" - }, - { - "type": "integer", - "description": "Offset for pagination (default 0)", - "name": "Offset", - "in": "query" - } - ], - "responses": { - "200": { - "description": "List of studios", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/response.StudioList" - } - } - } - ] - } - }, - "400": { - "description": "Bad request", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - } - } - } - }, - "/api/v1/studio/{id}": { - "get": { - "description": "Get details of a studio based on the provided ID.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Studio APIs" - ], - "summary": "Get details of a studio by ID", - "parameters": [ - { - "type": "string", - "description": "JWT token", - "name": "Authorization", - "in": "header", - "required": true - }, - { - "type": "integer", - "description": "Studio ID to retrieve", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "Studio details", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/response.Studio" - } - } - } - ] - } - }, - "400": { - "description": "Bad request", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - } - } - }, - "put": { - "description": "Update the details of an existing studio based on the provided ID.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Studio APIs" - ], - "summary": "Update an existing studio", - "parameters": [ - { - "type": "string", - "description": "JWT token", - "name": "Authorization", - "in": "header", - "required": true - }, - { - "type": "integer", - "description": "Studio ID to update", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "Updated studio details", - "name": "req", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.Studio" - } - } - ], - "responses": { - "200": { - "description": "Studio updated successfully", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/response.Studio" - } - } - } - ] - } - }, - "400": { - "description": "Bad request", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - } - } - } - }, - "/api/v1/user": { - "post": { - "description": "Create a new user based on the provided data.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "User APIs" - ], - "summary": "Create a new user", - "parameters": [ - { - "type": "string", - "description": "JWT token", - "name": "Authorization", - "in": "header", - "required": true - }, - { - "description": "New user details", - "name": "req", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.User" - } - } - ], - "responses": { - "200": { - "description": "User created successfully", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/response.User" - } - } - } - ] - } - }, - "400": { - "description": "Bad request", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - } - } - } - }, - "/api/v1/user/list": { - "get": { - "description": "Get a paginated list of users based on query parameters.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "User APIs" - ], - "summary": "Get a list of users", - "parameters": [ - { - "type": "string", - "description": "JWT token", - "name": "Authorization", - "in": "header", - "required": true - }, - { - "type": "integer", - "description": "Number of items to retrieve (default 10)", - "name": "Limit", - "in": "query" - }, - { - "type": "integer", - "description": "Offset for pagination (default 0)", - "name": "Offset", - "in": "query" - } - ], - "responses": { - "200": { - "description": "List of users", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/response.UserList" - } - } - } - ] - } - }, - "400": { - "description": "Bad request", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - } - } - } - }, - "/api/v1/user/{id}": { - "get": { - "description": "Get details of a user based on the provided ID.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "User APIs" - ], - "summary": "Get details of a user by ID", - "parameters": [ - { - "type": "string", - "description": "JWT token", - "name": "Authorization", - "in": "header", - "required": true - }, - { - "type": "integer", - "description": "User ID to retrieve", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "User details", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/response.User" - } - } - } - ] - } - }, - "400": { - "description": "Bad request", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - } - } - }, - "put": { - "description": "Update the details of an existing user based on the provided ID.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "User APIs" - ], - "summary": "Update an existing user", - "parameters": [ - { - "type": "string", - "description": "JWT token", - "name": "Authorization", - "in": "header", - "required": true - }, - { - "type": "integer", - "description": "User ID to update", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "Updated user details", - "name": "req", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.User" - } - } - ], - "responses": { - "200": { - "description": "User updated successfully", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/response.User" - } - } - } - ] - } - }, - "400": { - "description": "Bad request", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - } - } - }, - "delete": { - "description": "Delete a user based on the provided ID.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "User APIs" - ], - "summary": "Delete a user by ID", - "parameters": [ - { - "type": "string", - "description": "JWT token", - "name": "Authorization", - "in": "header", - "required": true - }, - { - "type": "integer", - "description": "User ID to delete", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "User deleted successfully", - "schema": { - "$ref": "#/definitions/response.BaseResponse" - } - }, - "400": { - "description": "Bad request", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - } - } - } - } - }, - "definitions": { - "branch.BranchStatus": { - "type": "string", - "enum": [ - "Active", - "Inactive" - ], - "x-enum-varnames": [ - "Active", - "Inactive" - ] - }, - "entity.UploadFileResponse": { - "type": "object", - "properties": { - "file_path": { - "type": "string" - }, - "file_url": { - "type": "string" - } - } - }, - "order.ItemType": { - "type": "string", - "enum": [ - "PRODUCT", - "STUDIO" - ], - "x-enum-varnames": [ - "Product", - "Studio" - ] - }, - "order.OrderStatus": { - "type": "string", - "enum": [ - "NEW", - "PAID", - "CANCEL" - ], - "x-enum-varnames": [ - "New", - "Paid", - "Cancel" - ] - }, - "product.ProductStatus": { - "type": "string", - "enum": [ - "Active", - "Inactive" - ], - "x-enum-varnames": [ - "Active", - "Inactive" - ] - }, - "product.ProductType": { - "type": "string", - "enum": [ - "FOOD", - "BEVERAGE" - ], - "x-enum-varnames": [ - "Food", - "Beverage" - ] - }, - "request.Branch": { - "type": "object", - "required": [ - "location", - "name" - ], - "properties": { - "location": { - "type": "string" - }, - "name": { - "type": "string" - }, - "status": { - "$ref": "#/definitions/branch.BranchStatus" - } - } - }, - "request.LoginRequest": { - "type": "object", - "properties": { - "email": { - "type": "string" - }, - "password": { - "type": "string" - } - } - }, - "request.Order": { - "type": "object", - "required": [ - "amount", - "branch_id", - "customer_name", - "customer_phone", - "order_items", - "pax", - "payment_method" - ], - "properties": { - "amount": { - "type": "number" - }, - "branch_id": { - "type": "integer" - }, - "customer_name": { - "type": "string" - }, - "customer_phone": { - "type": "string" - }, - "order_items": { - "type": "array", - "items": { - "$ref": "#/definitions/request.OrderItem" - } - }, - "pax": { - "type": "integer" - }, - "payment_method": { - "$ref": "#/definitions/transaction.Provider" - } - } - }, - "request.OrderItem": { - "type": "object", - "required": [ - "item_id", - "item_type", - "price", - "qty" - ], - "properties": { - "item_id": { - "type": "integer" - }, - "item_type": { - "$ref": "#/definitions/order.ItemType" - }, - "price": { - "type": "number" - }, - "qty": { - "type": "integer" - } - } - }, - "request.Product": { - "type": "object", - "required": [ - "branch_id", - "name", - "price", - "status", - "type" - ], - "properties": { - "branch_id": { - "type": "integer" - }, - "description": { - "type": "string" - }, - "name": { - "type": "string" - }, - "price": { - "type": "number" - }, - "status": { - "$ref": "#/definitions/product.ProductStatus" - }, - "stock_qty": { - "type": "integer" - }, - "type": { - "$ref": "#/definitions/product.ProductType" - } - } - }, - "request.Studio": { - "type": "object", - "required": [ - "branch_id", - "name", - "price" - ], - "properties": { - "branch_id": { - "type": "integer" - }, - "metadata": { - "type": "object", - "additionalProperties": true - }, - "name": { - "type": "string" - }, - "price": { - "type": "number" - }, - "status": { - "$ref": "#/definitions/studio.StudioStatus" - } - } - }, - "request.UpdateStatus": { - "type": "object", - "properties": { - "status": { - "allOf": [ - { - "$ref": "#/definitions/order.OrderStatus" - } - ], - "example": "NEW,PAID,CANCEL" - } - } - }, - "request.User": { - "type": "object", - "required": [ - "email", - "name", - "password", - "role_id" - ], - "properties": { - "branch_id": { - "type": "integer" - }, - "email": { - "type": "string" - }, - "name": { - "type": "string" - }, - "password": { - "type": "string" - }, - "role_id": { - "type": "integer" - } - } - }, - "response.BaseResponse": { - "type": "object", - "properties": { - "data": {}, - "error_detail": {}, - "error_message": { - "type": "string" - }, - "message": { - "type": "string" - }, - "meta": { - "$ref": "#/definitions/response.PagingMeta" - }, - "response_code": { - "type": "string" - }, - "success": { - "type": "boolean" - } - } - }, - "response.Branch": { - "type": "object", - "properties": { - "created_at": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "location": { - "type": "string" - }, - "name": { - "type": "string" - }, - "status": { - "type": "string" - }, - "updated_at": { - "type": "string" - } - } - }, - "response.BranchList": { - "type": "object", - "properties": { - "branches": { - "type": "array", - "items": { - "$ref": "#/definitions/response.Branch" - } - }, - "limit": { - "type": "integer" - }, - "offset": { - "type": "integer" - }, - "total": { - "type": "integer" - } - } - }, - "response.LoginResponse": { - "type": "object", - "properties": { - "branch": { - "$ref": "#/definitions/response.Branch" - }, - "name": { - "type": "string" - }, - "role": { - "$ref": "#/definitions/response.Role" - }, - "token": { - "type": "string" - } - } - }, - "response.Order": { - "type": "object", - "properties": { - "amount": { - "type": "number" - }, - "branch_id": { - "type": "integer" - }, - "branch_name": { - "type": "string" - }, - "created_at": { - "type": "string" - }, - "customer_name": { - "type": "string" - }, - "customer_phone": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "order_items": { - "type": "array", - "items": { - "$ref": "#/definitions/response.OrderItem" - } - }, - "pax": { - "type": "integer" - }, - "payment_method": { - "$ref": "#/definitions/transaction.Provider" - }, - "status": { - "$ref": "#/definitions/order.OrderStatus" - }, - "updated_at": { - "type": "string" - } - } - }, - "response.OrderBranchRevenue": { - "type": "object", - "properties": { - "branch_id": { - "type": "string" - }, - "location": { - "type": "string" - }, - "name": { - "type": "string" - }, - "total_amount": { - "type": "number" - }, - "total_trans": { - "type": "integer" - } - } - }, - "response.OrderItem": { - "type": "object", - "properties": { - "created_at": { - "type": "string" - }, - "item_id": { - "type": "integer" - }, - "item_name": { - "type": "string" - }, - "item_type": { - "$ref": "#/definitions/order.ItemType" - }, - "order_item_id": { - "type": "integer" - }, - "price": { - "type": "number" - }, - "qty": { - "type": "integer" - }, - "updated_at": { - "type": "string" - } - } - }, - "response.OrderList": { - "type": "object", - "properties": { - "limit": { - "type": "integer" - }, - "offset": { - "type": "integer" - }, - "orders": { - "type": "array", - "items": { - "$ref": "#/definitions/response.Order" - } - }, - "total": { - "type": "integer" - } - } - }, - "response.OrderMonthlyRevenue": { - "type": "object", - "properties": { - "total_revenue": { - "type": "number" - }, - "total_transaction": { - "type": "integer" - } - } - }, - "response.PagingMeta": { - "type": "object", - "properties": { - "limit": { - "type": "integer" - }, - "page": { - "type": "integer" - }, - "total_data": { - "type": "integer" - } - } - }, - "response.Product": { - "type": "object", - "properties": { - "branch_id": { - "type": "integer" - }, - "created_at": { - "type": "string" - }, - "description": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "name": { - "type": "string" - }, - "price": { - "type": "number" - }, - "status": { - "$ref": "#/definitions/product.ProductStatus" - }, - "stock_qty": { - "type": "integer" - }, - "type": { - "$ref": "#/definitions/product.ProductType" - }, - "updated_at": { - "type": "string" - } - } - }, - "response.ProductList": { - "type": "object", - "properties": { - "limit": { - "type": "integer" - }, - "offset": { - "type": "integer" - }, - "products": { - "type": "array", - "items": { - "$ref": "#/definitions/response.Product" - } - }, - "total": { - "type": "integer" - } - } - }, - "response.Role": { - "type": "object", - "properties": { - "id": { - "type": "integer" - }, - "role_name": { - "type": "string" - } - } - }, - "response.Studio": { - "type": "object", - "properties": { - "branch_id": { - "type": "integer" - }, - "created_at": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "metadata": { - "type": "object", - "additionalProperties": true - }, - "name": { - "type": "string" - }, - "price": { - "type": "number" - }, - "status": { - "type": "string" - }, - "updated_at": { - "type": "string" - } - } - }, - "response.StudioList": { - "type": "object", - "properties": { - "limit": { - "type": "integer" - }, - "offset": { - "type": "integer" - }, - "studios": { - "type": "array", - "items": { - "$ref": "#/definitions/response.Studio" - } - }, - "total": { - "type": "integer" - } - } - }, - "response.User": { - "type": "object", - "properties": { - "branch_id": { - "type": "integer" - }, - "branch_name": { - "type": "string" - }, - "created_at": { - "type": "string" - }, - "email": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "name": { - "type": "string" - }, - "role_id": { - "type": "integer" - }, - "role_name": { - "type": "string" - }, - "status": { - "type": "string" - }, - "updated_at": { - "type": "string" - } - } - }, - "response.UserList": { - "type": "object", - "properties": { - "limit": { - "type": "integer" - }, - "offset": { - "type": "integer" - }, - "total": { - "type": "integer" - }, - "users": { - "type": "array", - "items": { - "$ref": "#/definitions/response.User" - } - } - } - }, - "studio.StudioStatus": { - "type": "string", - "enum": [ - "Active", - "Inactive" - ], - "x-enum-varnames": [ - "Active", - "Inactive" - ] - }, - "transaction.Provider": { - "type": "string", - "enum": [ - "CASH", - "DEBIT", - "TRANSFER", - "QRIS" - ], - "x-enum-varnames": [ - "Cash", - "Debit", - "Transfer", - "QRIS" - ] - } - } -}` - -// SwaggerInfo holds exported Swagger Info so clients can modify it -var SwaggerInfo = &swag.Spec{ - Version: "", - Host: "", - BasePath: "", - Schemes: []string{}, - Title: "", - Description: "", - InfoInstanceName: "swagger", - SwaggerTemplate: docTemplate, - LeftDelim: "{{", - RightDelim: "}}", -} - -func init() { - swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) -} diff --git a/docs/swagger.json b/docs/swagger.json deleted file mode 100644 index cfd5d11..0000000 --- a/docs/swagger.json +++ /dev/null @@ -1,3072 +0,0 @@ -{ - "swagger": "2.0", - "info": { - "contact": {} - }, - "paths": { - "/api/v1/auth/login": { - "post": { - "description": "Authenticates a user based on the provided credentials and returns a JWT token.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Auth Login API's" - ], - "summary": "User login", - "parameters": [ - { - "description": "User login credentials", - "name": "bodyParam", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.LoginRequest" - } - } - ], - "responses": { - "200": { - "description": "Login successful", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/response.LoginResponse" - } - } - } - ] - } - }, - "400": { - "description": "Bad request", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - } - } - } - }, - "/api/v1/branch": { - "post": { - "description": "Create a new branch based on the provided data.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Branch APIs" - ], - "summary": "Create a new branch", - "parameters": [ - { - "type": "string", - "description": "JWT token", - "name": "Authorization", - "in": "header", - "required": true - }, - { - "description": "New branch details", - "name": "req", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.Branch" - } - } - ], - "responses": { - "200": { - "description": "Branch created successfully", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/response.Branch" - } - } - } - ] - } - }, - "400": { - "description": "Bad request", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - } - } - } - }, - "/api/v1/branch/list": { - "get": { - "description": "Get a paginated list of branches based on query parameters.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Branch APIs" - ], - "summary": "Get a list of branches", - "parameters": [ - { - "type": "string", - "description": "JWT token", - "name": "Authorization", - "in": "header", - "required": true - }, - { - "type": "integer", - "description": "Number of items to retrieve (default 10)", - "name": "Limit", - "in": "query" - }, - { - "type": "integer", - "description": "Offset for pagination (default 0)", - "name": "Offset", - "in": "query" - } - ], - "responses": { - "200": { - "description": "List of branches", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/response.BranchList" - } - } - } - ] - } - }, - "400": { - "description": "Bad request", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - } - } - } - }, - "/api/v1/branch/{id}": { - "get": { - "description": "Get details of a branch based on the provided ID.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Branch APIs" - ], - "summary": "Get details of a branch by ID", - "parameters": [ - { - "type": "string", - "description": "JWT token", - "name": "Authorization", - "in": "header", - "required": true - }, - { - "type": "integer", - "description": "Branch ID to retrieve", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "Branch details", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/response.Branch" - } - } - } - ] - } - }, - "400": { - "description": "Bad request", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - } - } - }, - "put": { - "description": "Update the details of an existing branch based on the provided ID.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Branch APIs" - ], - "summary": "Update an existing branch", - "parameters": [ - { - "type": "string", - "description": "JWT token", - "name": "Authorization", - "in": "header", - "required": true - }, - { - "type": "integer", - "description": "Branch ID to update", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "Updated branch details", - "name": "req", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.Branch" - } - } - ], - "responses": { - "200": { - "description": "Branch updated successfully", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/response.Branch" - } - } - } - ] - } - }, - "400": { - "description": "Bad request", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - } - } - }, - "delete": { - "description": "Delete a branch based on the provided ID.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Branch APIs" - ], - "summary": "Delete a branch by ID", - "parameters": [ - { - "type": "string", - "description": "JWT token", - "name": "Authorization", - "in": "header", - "required": true - }, - { - "type": "integer", - "description": "Branch ID to delete", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "Branch deleted successfully", - "schema": { - "$ref": "#/definitions/response.BaseResponse" - } - }, - "400": { - "description": "Bad request", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - } - } - } - }, - "/api/v1/file/upload": { - "post": { - "description": "Upload a file to Alibaba Cloud OSS with the provided details.", - "consumes": [ - "multipart/form-data" - ], - "produces": [ - "application/json" - ], - "tags": [ - "File Upload API" - ], - "summary": "Upload a file to OSS", - "parameters": [ - { - "type": "string", - "description": "JWT token", - "name": "Authorization", - "in": "header", - "required": true - }, - { - "type": "file", - "description": "File to upload (max size: 2MB)", - "name": "file", - "in": "formData", - "required": true - } - ], - "responses": { - "200": { - "description": "File uploaded successfully", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/entity.UploadFileResponse" - } - } - } - ] - } - }, - "400": { - "description": "Bad request", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - } - } - } - }, - "/api/v1/order": { - "post": { - "description": "Create a new order with the provided details.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "summary": "Create a new order", - "parameters": [ - { - "type": "string", - "description": "JWT token", - "name": "Authorization", - "in": "header", - "required": true - }, - { - "description": "Order details", - "name": "order", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.Order" - } - } - ], - "responses": { - "200": { - "description": "Order created successfully", - "schema": { - "$ref": "#/definitions/response.BaseResponse" - } - }, - "400": { - "description": "Bad request", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - } - } - } - }, - "/api/v1/order/branch-revenue": { - "get": { - "description": "Retrieve the branch-wise revenue for orders based on the specified parameters.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "summary": "Get branch-wise revenue for orders", - "parameters": [ - { - "type": "string", - "description": "JWT token", - "name": "Authorization", - "in": "header", - "required": true - }, - { - "type": "integer", - "description": "Branch ID for filtering", - "name": "branch_id", - "in": "query" - }, - { - "type": "string", - "description": "Start date for filtering (format: 'YYYY-MM-DD')", - "name": "start_date", - "in": "query" - }, - { - "type": "string", - "description": "End date for filtering (format: 'YYYY-MM-DD')", - "name": "end_date", - "in": "query" - } - ], - "responses": { - "200": { - "description": "Branch-wise revenue retrieved successfully", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/response.OrderBranchRevenue" - } - } - } - } - ] - } - }, - "400": { - "description": "Bad request", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "500": { - "description": "Internal server error", - "schema": { - "$ref": "#/definitions/response.BaseResponse" - } - } - } - } - }, - "/api/v1/order/list": { - "get": { - "description": "Retrieve a list of orders based on the specified parameters.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "summary": "Get a list of orders", - "parameters": [ - { - "type": "string", - "description": "JWT token", - "name": "Authorization", - "in": "header", - "required": true - }, - { - "type": "integer", - "description": "Number of items to retrieve (default: 10)", - "name": "limit", - "in": "query" - }, - { - "type": "integer", - "description": "Number of items to skip (default: 0)", - "name": "offset", - "in": "query" - } - ], - "responses": { - "200": { - "description": "List of orders retrieved successfully", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/response.OrderList" - } - } - } - ] - } - }, - "400": { - "description": "Bad request", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "500": { - "description": "Internal server error", - "schema": { - "$ref": "#/definitions/response.BaseResponse" - } - } - } - } - }, - "/api/v1/order/total-revenue": { - "get": { - "description": "Retrieve the total revenue and number of transactions for orders based on the specified parameters.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "summary": "Get total revenue and number of transactions for orders", - "parameters": [ - { - "type": "string", - "description": "JWT token", - "name": "Authorization", - "in": "header", - "required": true - }, - { - "type": "string", - "description": "Start date for filtering (format: 'YYYY-MM-DD')", - "name": "start_date", - "in": "query" - }, - { - "type": "string", - "description": "End date for filtering (format: 'YYYY-MM-DD')", - "name": "end_date", - "in": "query" - } - ], - "responses": { - "200": { - "description": "Total revenue and transactions retrieved successfully", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/response.OrderMonthlyRevenue" - } - } - } - ] - } - }, - "400": { - "description": "Bad request", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "500": { - "description": "Internal server error", - "schema": { - "$ref": "#/definitions/response.BaseResponse" - } - } - } - } - }, - "/api/v1/order/update-status/{id}": { - "put": { - "description": "Update the status of the specified order with the provided details.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "summary": "Update the status of an order", - "parameters": [ - { - "type": "string", - "description": "JWT token", - "name": "Authorization", - "in": "header", - "required": true - }, - { - "type": "string", - "description": "Order ID", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "Status details", - "name": "status", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.UpdateStatus" - } - } - ], - "responses": { - "200": { - "description": "Order status updated successfully", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/response.Order" - } - } - } - ] - } - }, - "400": { - "description": "Bad request", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "500": { - "description": "Internal server error", - "schema": { - "$ref": "#/definitions/response.BaseResponse" - } - } - } - } - }, - "/api/v1/order/yearly-revenue/{year}": { - "get": { - "description": "Retrieve the yearly revenue for orders based on the specified year.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "summary": "Get yearly revenue for orders", - "parameters": [ - { - "type": "string", - "description": "JWT token", - "name": "Authorization", - "in": "header", - "required": true - }, - { - "type": "integer", - "description": "Year for filtering", - "name": "year", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "Yearly revenue retrieved successfully", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": { - "type": "object", - "additionalProperties": { - "type": "object", - "additionalProperties": { - "type": "number" - } - } - } - } - } - ] - } - }, - "400": { - "description": "Bad request", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "500": { - "description": "Internal server error", - "schema": { - "$ref": "#/definitions/response.BaseResponse" - } - } - } - } - }, - "/api/v1/order/{id}": { - "get": { - "description": "Retrieve the details of the specified order by ID.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "summary": "Get details of an order by ID", - "parameters": [ - { - "type": "string", - "description": "JWT token", - "name": "Authorization", - "in": "header", - "required": true - }, - { - "type": "string", - "description": "Order ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "Order details retrieved successfully", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/response.Order" - } - } - } - ] - } - }, - "400": { - "description": "Bad request", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "500": { - "description": "Internal server error", - "schema": { - "$ref": "#/definitions/response.BaseResponse" - } - } - } - } - }, - "/api/v1/product/": { - "post": { - "description": "Create a new product with the provided details.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Product APIs" - ], - "summary": "Create a new product", - "parameters": [ - { - "type": "string", - "description": "JWT token", - "name": "Authorization", - "in": "header", - "required": true - }, - { - "description": "Product details to create", - "name": "req", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.Product" - } - } - ], - "responses": { - "200": { - "description": "Product created successfully", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/response.Product" - } - } - } - ] - } - }, - "400": { - "description": "Bad request", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - } - } - } - }, - "/api/v1/product/list": { - "get": { - "description": "Get a paginated list of products based on query parameters.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Product APIs" - ], - "summary": "Get a list of products", - "parameters": [ - { - "type": "string", - "description": "JWT token", - "name": "Authorization", - "in": "header", - "required": true - }, - { - "type": "integer", - "description": "Number of items to retrieve (default 10)", - "name": "Limit", - "in": "query" - }, - { - "type": "integer", - "description": "Offset for pagination (default 0)", - "name": "Offset", - "in": "query" - } - ], - "responses": { - "200": { - "description": "List of products", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/response.ProductList" - } - } - } - ] - } - }, - "400": { - "description": "Bad request", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - } - } - } - }, - "/api/v1/product/{id}": { - "get": { - "description": "Get details of a product based on the provided ID.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Product APIs" - ], - "summary": "Get details of a product by ID", - "parameters": [ - { - "type": "string", - "description": "JWT token", - "name": "Authorization", - "in": "header", - "required": true - }, - { - "type": "integer", - "description": "Product ID to retrieve", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "Product details", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/response.Product" - } - } - } - ] - } - }, - "400": { - "description": "Bad request", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - } - } - }, - "put": { - "description": "Update the details of an existing product based on the provided ID.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Product APIs" - ], - "summary": "Update an existing product", - "parameters": [ - { - "type": "string", - "description": "JWT token", - "name": "Authorization", - "in": "header", - "required": true - }, - { - "type": "integer", - "description": "Product ID to update", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "Updated product details", - "name": "req", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.Product" - } - } - ], - "responses": { - "200": { - "description": "Product updated successfully", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/response.Product" - } - } - } - ] - } - }, - "400": { - "description": "Bad request", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - } - } - }, - "delete": { - "description": "Delete a product based on the provided ID.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Product APIs" - ], - "summary": "Delete a product by ID", - "parameters": [ - { - "type": "string", - "description": "JWT token", - "name": "Authorization", - "in": "header", - "required": true - }, - { - "type": "integer", - "description": "Product ID to delete", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "Product deleted successfully", - "schema": { - "$ref": "#/definitions/response.BaseResponse" - } - }, - "400": { - "description": "Bad request", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - } - } - } - }, - "/api/v1/studio": { - "post": { - "description": "Create a new studio based on the provided details.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Studio APIs" - ], - "summary": "Create a new studio", - "parameters": [ - { - "type": "string", - "description": "JWT token", - "name": "Authorization", - "in": "header", - "required": true - }, - { - "description": "New studio details", - "name": "req", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.Studio" - } - } - ], - "responses": { - "200": { - "description": "Studio created successfully", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/response.Studio" - } - } - } - ] - } - }, - "400": { - "description": "Bad request", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - } - } - } - }, - "/api/v1/studio/search": { - "get": { - "description": "Search for studios based on query parameters.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Studio APIs" - ], - "summary": "Search for studios", - "parameters": [ - { - "type": "string", - "description": "JWT token", - "name": "Authorization", - "in": "header", - "required": true - }, - { - "type": "string", - "description": "Studio name for search", - "name": "Name", - "in": "query" - }, - { - "type": "string", - "description": "Studio status for search", - "name": "Status", - "in": "query" - }, - { - "type": "integer", - "description": "Number of items to retrieve (default 10)", - "name": "Limit", - "in": "query" - }, - { - "type": "integer", - "description": "Offset for pagination (default 0)", - "name": "Offset", - "in": "query" - } - ], - "responses": { - "200": { - "description": "List of studios", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/response.StudioList" - } - } - } - ] - } - }, - "400": { - "description": "Bad request", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - } - } - } - }, - "/api/v1/studio/{id}": { - "get": { - "description": "Get details of a studio based on the provided ID.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Studio APIs" - ], - "summary": "Get details of a studio by ID", - "parameters": [ - { - "type": "string", - "description": "JWT token", - "name": "Authorization", - "in": "header", - "required": true - }, - { - "type": "integer", - "description": "Studio ID to retrieve", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "Studio details", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/response.Studio" - } - } - } - ] - } - }, - "400": { - "description": "Bad request", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - } - } - }, - "put": { - "description": "Update the details of an existing studio based on the provided ID.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Studio APIs" - ], - "summary": "Update an existing studio", - "parameters": [ - { - "type": "string", - "description": "JWT token", - "name": "Authorization", - "in": "header", - "required": true - }, - { - "type": "integer", - "description": "Studio ID to update", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "Updated studio details", - "name": "req", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.Studio" - } - } - ], - "responses": { - "200": { - "description": "Studio updated successfully", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/response.Studio" - } - } - } - ] - } - }, - "400": { - "description": "Bad request", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - } - } - } - }, - "/api/v1/user": { - "post": { - "description": "Create a new user based on the provided data.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "User APIs" - ], - "summary": "Create a new user", - "parameters": [ - { - "type": "string", - "description": "JWT token", - "name": "Authorization", - "in": "header", - "required": true - }, - { - "description": "New user details", - "name": "req", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.User" - } - } - ], - "responses": { - "200": { - "description": "User created successfully", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/response.User" - } - } - } - ] - } - }, - "400": { - "description": "Bad request", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - } - } - } - }, - "/api/v1/user/list": { - "get": { - "description": "Get a paginated list of users based on query parameters.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "User APIs" - ], - "summary": "Get a list of users", - "parameters": [ - { - "type": "string", - "description": "JWT token", - "name": "Authorization", - "in": "header", - "required": true - }, - { - "type": "integer", - "description": "Number of items to retrieve (default 10)", - "name": "Limit", - "in": "query" - }, - { - "type": "integer", - "description": "Offset for pagination (default 0)", - "name": "Offset", - "in": "query" - } - ], - "responses": { - "200": { - "description": "List of users", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/response.UserList" - } - } - } - ] - } - }, - "400": { - "description": "Bad request", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - } - } - } - }, - "/api/v1/user/{id}": { - "get": { - "description": "Get details of a user based on the provided ID.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "User APIs" - ], - "summary": "Get details of a user by ID", - "parameters": [ - { - "type": "string", - "description": "JWT token", - "name": "Authorization", - "in": "header", - "required": true - }, - { - "type": "integer", - "description": "User ID to retrieve", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "User details", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/response.User" - } - } - } - ] - } - }, - "400": { - "description": "Bad request", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - } - } - }, - "put": { - "description": "Update the details of an existing user based on the provided ID.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "User APIs" - ], - "summary": "Update an existing user", - "parameters": [ - { - "type": "string", - "description": "JWT token", - "name": "Authorization", - "in": "header", - "required": true - }, - { - "type": "integer", - "description": "User ID to update", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "Updated user details", - "name": "req", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/request.User" - } - } - ], - "responses": { - "200": { - "description": "User updated successfully", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/response.User" - } - } - } - ] - } - }, - "400": { - "description": "Bad request", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - } - } - }, - "delete": { - "description": "Delete a user based on the provided ID.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "User APIs" - ], - "summary": "Delete a user by ID", - "parameters": [ - { - "type": "string", - "description": "JWT token", - "name": "Authorization", - "in": "header", - "required": true - }, - { - "type": "integer", - "description": "User ID to delete", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "User deleted successfully", - "schema": { - "$ref": "#/definitions/response.BaseResponse" - } - }, - "400": { - "description": "Bad request", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.BaseResponse" - }, - { - "type": "object", - "properties": { - "data": {} - } - } - ] - } - } - } - } - } - }, - "definitions": { - "branch.BranchStatus": { - "type": "string", - "enum": [ - "Active", - "Inactive" - ], - "x-enum-varnames": [ - "Active", - "Inactive" - ] - }, - "entity.UploadFileResponse": { - "type": "object", - "properties": { - "file_path": { - "type": "string" - }, - "file_url": { - "type": "string" - } - } - }, - "order.ItemType": { - "type": "string", - "enum": [ - "PRODUCT", - "STUDIO" - ], - "x-enum-varnames": [ - "Product", - "Studio" - ] - }, - "order.OrderStatus": { - "type": "string", - "enum": [ - "NEW", - "PAID", - "CANCEL" - ], - "x-enum-varnames": [ - "New", - "Paid", - "Cancel" - ] - }, - "product.ProductStatus": { - "type": "string", - "enum": [ - "Active", - "Inactive" - ], - "x-enum-varnames": [ - "Active", - "Inactive" - ] - }, - "product.ProductType": { - "type": "string", - "enum": [ - "FOOD", - "BEVERAGE" - ], - "x-enum-varnames": [ - "Food", - "Beverage" - ] - }, - "request.Branch": { - "type": "object", - "required": [ - "location", - "name" - ], - "properties": { - "location": { - "type": "string" - }, - "name": { - "type": "string" - }, - "status": { - "$ref": "#/definitions/branch.BranchStatus" - } - } - }, - "request.LoginRequest": { - "type": "object", - "properties": { - "email": { - "type": "string" - }, - "password": { - "type": "string" - } - } - }, - "request.Order": { - "type": "object", - "required": [ - "amount", - "branch_id", - "customer_name", - "customer_phone", - "order_items", - "pax", - "payment_method" - ], - "properties": { - "amount": { - "type": "number" - }, - "branch_id": { - "type": "integer" - }, - "customer_name": { - "type": "string" - }, - "customer_phone": { - "type": "string" - }, - "order_items": { - "type": "array", - "items": { - "$ref": "#/definitions/request.OrderItem" - } - }, - "pax": { - "type": "integer" - }, - "payment_method": { - "$ref": "#/definitions/transaction.PaymentMethod" - } - } - }, - "request.OrderItem": { - "type": "object", - "required": [ - "item_id", - "item_type", - "price", - "qty" - ], - "properties": { - "item_id": { - "type": "integer" - }, - "item_type": { - "$ref": "#/definitions/order.ItemType" - }, - "price": { - "type": "number" - }, - "qty": { - "type": "integer" - } - } - }, - "request.Product": { - "type": "object", - "required": [ - "branch_id", - "name", - "price", - "status", - "type" - ], - "properties": { - "branch_id": { - "type": "integer" - }, - "description": { - "type": "string" - }, - "name": { - "type": "string" - }, - "price": { - "type": "number" - }, - "status": { - "$ref": "#/definitions/product.ProductStatus" - }, - "stock_qty": { - "type": "integer" - }, - "type": { - "$ref": "#/definitions/product.ProductType" - } - } - }, - "request.Studio": { - "type": "object", - "required": [ - "branch_id", - "name", - "price" - ], - "properties": { - "branch_id": { - "type": "integer" - }, - "metadata": { - "type": "object", - "additionalProperties": true - }, - "name": { - "type": "string" - }, - "price": { - "type": "number" - }, - "status": { - "$ref": "#/definitions/studio.StudioStatus" - } - } - }, - "request.UpdateStatus": { - "type": "object", - "properties": { - "status": { - "allOf": [ - { - "$ref": "#/definitions/order.OrderStatus" - } - ], - "example": "NEW,PAID,CANCEL" - } - } - }, - "request.User": { - "type": "object", - "required": [ - "email", - "name", - "password", - "role_id" - ], - "properties": { - "branch_id": { - "type": "integer" - }, - "email": { - "type": "string" - }, - "name": { - "type": "string" - }, - "password": { - "type": "string" - }, - "role_id": { - "type": "integer" - } - } - }, - "response.BaseResponse": { - "type": "object", - "properties": { - "data": {}, - "error_detail": {}, - "error_message": { - "type": "string" - }, - "message": { - "type": "string" - }, - "meta": { - "$ref": "#/definitions/response.PagingMeta" - }, - "response_code": { - "type": "string" - }, - "success": { - "type": "boolean" - } - } - }, - "response.Branch": { - "type": "object", - "properties": { - "created_at": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "location": { - "type": "string" - }, - "name": { - "type": "string" - }, - "status": { - "type": "string" - }, - "updated_at": { - "type": "string" - } - } - }, - "response.BranchList": { - "type": "object", - "properties": { - "branches": { - "type": "array", - "items": { - "$ref": "#/definitions/response.Branch" - } - }, - "limit": { - "type": "integer" - }, - "offset": { - "type": "integer" - }, - "total": { - "type": "integer" - } - } - }, - "response.LoginResponse": { - "type": "object", - "properties": { - "branch": { - "$ref": "#/definitions/response.Branch" - }, - "name": { - "type": "string" - }, - "role": { - "$ref": "#/definitions/response.Role" - }, - "token": { - "type": "string" - } - } - }, - "response.Order": { - "type": "object", - "properties": { - "amount": { - "type": "number" - }, - "branch_id": { - "type": "integer" - }, - "branch_name": { - "type": "string" - }, - "created_at": { - "type": "string" - }, - "customer_name": { - "type": "string" - }, - "customer_phone": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "order_items": { - "type": "array", - "items": { - "$ref": "#/definitions/response.OrderItem" - } - }, - "pax": { - "type": "integer" - }, - "payment_method": { - "$ref": "#/definitions/transaction.PaymentMethod" - }, - "status": { - "$ref": "#/definitions/order.OrderStatus" - }, - "updated_at": { - "type": "string" - } - } - }, - "response.OrderBranchRevenue": { - "type": "object", - "properties": { - "branch_id": { - "type": "string" - }, - "location": { - "type": "string" - }, - "name": { - "type": "string" - }, - "total_amount": { - "type": "number" - }, - "total_trans": { - "type": "integer" - } - } - }, - "response.OrderItem": { - "type": "object", - "properties": { - "created_at": { - "type": "string" - }, - "item_id": { - "type": "integer" - }, - "item_name": { - "type": "string" - }, - "item_type": { - "$ref": "#/definitions/order.ItemType" - }, - "order_item_id": { - "type": "integer" - }, - "price": { - "type": "number" - }, - "qty": { - "type": "integer" - }, - "updated_at": { - "type": "string" - } - } - }, - "response.OrderList": { - "type": "object", - "properties": { - "limit": { - "type": "integer" - }, - "offset": { - "type": "integer" - }, - "orders": { - "type": "array", - "items": { - "$ref": "#/definitions/response.Order" - } - }, - "total": { - "type": "integer" - } - } - }, - "response.OrderMonthlyRevenue": { - "type": "object", - "properties": { - "total_revenue": { - "type": "number" - }, - "total_transaction": { - "type": "integer" - } - } - }, - "response.PagingMeta": { - "type": "object", - "properties": { - "limit": { - "type": "integer" - }, - "page": { - "type": "integer" - }, - "total_data": { - "type": "integer" - } - } - }, - "response.Product": { - "type": "object", - "properties": { - "branch_id": { - "type": "integer" - }, - "created_at": { - "type": "string" - }, - "description": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "name": { - "type": "string" - }, - "price": { - "type": "number" - }, - "status": { - "$ref": "#/definitions/product.ProductStatus" - }, - "stock_qty": { - "type": "integer" - }, - "type": { - "$ref": "#/definitions/product.ProductType" - }, - "updated_at": { - "type": "string" - } - } - }, - "response.ProductList": { - "type": "object", - "properties": { - "limit": { - "type": "integer" - }, - "offset": { - "type": "integer" - }, - "products": { - "type": "array", - "items": { - "$ref": "#/definitions/response.Product" - } - }, - "total": { - "type": "integer" - } - } - }, - "response.Role": { - "type": "object", - "properties": { - "id": { - "type": "integer" - }, - "role_name": { - "type": "string" - } - } - }, - "response.Studio": { - "type": "object", - "properties": { - "branch_id": { - "type": "integer" - }, - "created_at": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "metadata": { - "type": "object", - "additionalProperties": true - }, - "name": { - "type": "string" - }, - "price": { - "type": "number" - }, - "status": { - "type": "string" - }, - "updated_at": { - "type": "string" - } - } - }, - "response.StudioList": { - "type": "object", - "properties": { - "limit": { - "type": "integer" - }, - "offset": { - "type": "integer" - }, - "studios": { - "type": "array", - "items": { - "$ref": "#/definitions/response.Studio" - } - }, - "total": { - "type": "integer" - } - } - }, - "response.User": { - "type": "object", - "properties": { - "branch_id": { - "type": "integer" - }, - "branch_name": { - "type": "string" - }, - "created_at": { - "type": "string" - }, - "email": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "name": { - "type": "string" - }, - "role_id": { - "type": "integer" - }, - "role_name": { - "type": "string" - }, - "status": { - "type": "string" - }, - "updated_at": { - "type": "string" - } - } - }, - "response.UserList": { - "type": "object", - "properties": { - "limit": { - "type": "integer" - }, - "offset": { - "type": "integer" - }, - "total": { - "type": "integer" - }, - "users": { - "type": "array", - "items": { - "$ref": "#/definitions/response.User" - } - } - } - }, - "studio.StudioStatus": { - "type": "string", - "enum": [ - "Active", - "Inactive" - ], - "x-enum-varnames": [ - "Active", - "Inactive" - ] - }, - "transaction.PaymentMethod": { - "type": "string", - "enum": [ - "CASH", - "DEBIT", - "TRANSFER", - "QRIS" - ], - "x-enum-varnames": [ - "Cash", - "Debit", - "Transfer", - "QRIS" - ] - } - } -} \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml deleted file mode 100644 index 726cecd..0000000 --- a/docs/swagger.yaml +++ /dev/null @@ -1,1833 +0,0 @@ -definitions: - branch.BranchStatus: - enum: - - Active - - Inactive - type: string - x-enum-varnames: - - Active - - Inactive - entity.UploadFileResponse: - properties: - file_path: - type: string - file_url: - type: string - type: object - order.ItemType: - enum: - - PRODUCT - - STUDIO - type: string - x-enum-varnames: - - Product - - Studio - order.OrderStatus: - enum: - - NEW - - PAID - - CANCEL - type: string - x-enum-varnames: - - New - - Paid - - Cancel - product.ProductStatus: - enum: - - Active - - Inactive - type: string - x-enum-varnames: - - Active - - Inactive - product.ProductType: - enum: - - FOOD - - BEVERAGE - type: string - x-enum-varnames: - - Food - - Beverage - request.Branch: - properties: - location: - type: string - name: - type: string - status: - $ref: '#/definitions/branch.BranchStatus' - required: - - location - - name - type: object - request.LoginRequest: - properties: - email: - type: string - password: - type: string - type: object - request.Order: - properties: - amount: - type: number - branch_id: - type: integer - customer_name: - type: string - customer_phone: - type: string - order_items: - items: - $ref: '#/definitions/request.OrderItem' - type: array - pax: - type: integer - payment_method: - $ref: '#/definitions/transaction.PaymentMethod' - required: - - amount - - branch_id - - customer_name - - customer_phone - - order_items - - pax - - payment_method - type: object - request.OrderItem: - properties: - item_id: - type: integer - item_type: - $ref: '#/definitions/order.ItemType' - price: - type: number - qty: - type: integer - required: - - item_id - - item_type - - price - - qty - type: object - request.Product: - properties: - branch_id: - type: integer - description: - type: string - name: - type: string - price: - type: number - status: - $ref: '#/definitions/product.ProductStatus' - stock_qty: - type: integer - type: - $ref: '#/definitions/product.ProductType' - required: - - branch_id - - name - - price - - status - - type - type: object - request.Studio: - properties: - branch_id: - type: integer - metadata: - additionalProperties: true - type: object - name: - type: string - price: - type: number - status: - $ref: '#/definitions/studio.StudioStatus' - required: - - branch_id - - name - - price - type: object - request.UpdateStatus: - properties: - status: - allOf: - - $ref: '#/definitions/order.OrderStatus' - example: NEW,PAID,CANCEL - type: object - request.User: - properties: - branch_id: - type: integer - email: - type: string - name: - type: string - password: - type: string - role_id: - type: integer - required: - - email - - name - - password - - role_id - type: object - response.BaseResponse: - properties: - data: {} - error_detail: {} - error_message: - type: string - message: - type: string - meta: - $ref: '#/definitions/response.PagingMeta' - response_code: - type: string - success: - type: boolean - type: object - response.Branch: - properties: - created_at: - type: string - id: - type: integer - location: - type: string - name: - type: string - status: - type: string - updated_at: - type: string - type: object - response.BranchList: - properties: - branches: - items: - $ref: '#/definitions/response.Branch' - type: array - limit: - type: integer - offset: - type: integer - total: - type: integer - type: object - response.LoginResponse: - properties: - branch: - $ref: '#/definitions/response.Branch' - name: - type: string - role: - $ref: '#/definitions/response.Role' - token: - type: string - type: object - response.Order: - properties: - amount: - type: number - branch_id: - type: integer - branch_name: - type: string - created_at: - type: string - customer_name: - type: string - customer_phone: - type: string - id: - type: integer - order_items: - items: - $ref: '#/definitions/response.OrderItem' - type: array - pax: - type: integer - payment_method: - $ref: '#/definitions/transaction.PaymentMethod' - status: - $ref: '#/definitions/order.OrderStatus' - updated_at: - type: string - type: object - response.OrderBranchRevenue: - properties: - branch_id: - type: string - location: - type: string - name: - type: string - total_amount: - type: number - total_trans: - type: integer - type: object - response.OrderItem: - properties: - created_at: - type: string - item_id: - type: integer - item_name: - type: string - item_type: - $ref: '#/definitions/order.ItemType' - order_item_id: - type: integer - price: - type: number - qty: - type: integer - updated_at: - type: string - type: object - response.OrderList: - properties: - limit: - type: integer - offset: - type: integer - orders: - items: - $ref: '#/definitions/response.Order' - type: array - total: - type: integer - type: object - response.OrderMonthlyRevenue: - properties: - total_revenue: - type: number - total_transaction: - type: integer - type: object - response.PagingMeta: - properties: - limit: - type: integer - page: - type: integer - total_data: - type: integer - type: object - response.Product: - properties: - branch_id: - type: integer - created_at: - type: string - description: - type: string - id: - type: integer - name: - type: string - price: - type: number - status: - $ref: '#/definitions/product.ProductStatus' - stock_qty: - type: integer - type: - $ref: '#/definitions/product.ProductType' - updated_at: - type: string - type: object - response.ProductList: - properties: - limit: - type: integer - offset: - type: integer - products: - items: - $ref: '#/definitions/response.Product' - type: array - total: - type: integer - type: object - response.Role: - properties: - id: - type: integer - role_name: - type: string - type: object - response.Studio: - properties: - branch_id: - type: integer - created_at: - type: string - id: - type: integer - metadata: - additionalProperties: true - type: object - name: - type: string - price: - type: number - status: - type: string - updated_at: - type: string - type: object - response.StudioList: - properties: - limit: - type: integer - offset: - type: integer - studios: - items: - $ref: '#/definitions/response.Studio' - type: array - total: - type: integer - type: object - response.User: - properties: - branch_id: - type: integer - branch_name: - type: string - created_at: - type: string - email: - type: string - id: - type: integer - name: - type: string - role_id: - type: integer - role_name: - type: string - status: - type: string - updated_at: - type: string - type: object - response.UserList: - properties: - limit: - type: integer - offset: - type: integer - total: - type: integer - users: - items: - $ref: '#/definitions/response.User' - type: array - type: object - studio.StudioStatus: - enum: - - Active - - Inactive - type: string - x-enum-varnames: - - Active - - Inactive - transaction.PaymentMethod: - enum: - - CASH - - DEBIT - - TRANSFER - - QRIS - type: string - x-enum-varnames: - - Cash - - Debit - - Transfer - - QRIS -info: - contact: {} -paths: - /api/v1/auth/login: - post: - consumes: - - application/json - description: Authenticates a user based on the provided credentials and returns - a JWT token. - parameters: - - description: User login credentials - in: body - name: bodyParam - required: true - schema: - $ref: '#/definitions/request.LoginRequest' - produces: - - application/json - responses: - "200": - description: Login successful - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: - $ref: '#/definitions/response.LoginResponse' - type: object - "400": - description: Bad request - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: {} - type: object - "401": - description: Unauthorized - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: {} - type: object - summary: User login - tags: - - Auth Login API's - /api/v1/branch: - post: - consumes: - - application/json - description: Create a new branch based on the provided data. - parameters: - - description: JWT token - in: header - name: Authorization - required: true - type: string - - description: New branch details - in: body - name: req - required: true - schema: - $ref: '#/definitions/request.Branch' - produces: - - application/json - responses: - "200": - description: Branch created successfully - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: - $ref: '#/definitions/response.Branch' - type: object - "400": - description: Bad request - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: {} - type: object - "401": - description: Unauthorized - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: {} - type: object - summary: Create a new branch - tags: - - Branch APIs - /api/v1/branch/{id}: - delete: - consumes: - - application/json - description: Delete a branch based on the provided ID. - parameters: - - description: JWT token - in: header - name: Authorization - required: true - type: string - - description: Branch ID to delete - in: path - name: id - required: true - type: integer - produces: - - application/json - responses: - "200": - description: Branch deleted successfully - schema: - $ref: '#/definitions/response.BaseResponse' - "400": - description: Bad request - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: {} - type: object - "401": - description: Unauthorized - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: {} - type: object - summary: Delete a branch by ID - tags: - - Branch APIs - get: - consumes: - - application/json - description: Get details of a branch based on the provided ID. - parameters: - - description: JWT token - in: header - name: Authorization - required: true - type: string - - description: Branch ID to retrieve - in: path - name: id - required: true - type: integer - produces: - - application/json - responses: - "200": - description: Branch details - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: - $ref: '#/definitions/response.Branch' - type: object - "400": - description: Bad request - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: {} - type: object - "401": - description: Unauthorized - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: {} - type: object - summary: Get details of a branch by ID - tags: - - Branch APIs - put: - consumes: - - application/json - description: Update the details of an existing branch based on the provided - ID. - parameters: - - description: JWT token - in: header - name: Authorization - required: true - type: string - - description: Branch ID to update - in: path - name: id - required: true - type: integer - - description: Updated branch details - in: body - name: req - required: true - schema: - $ref: '#/definitions/request.Branch' - produces: - - application/json - responses: - "200": - description: Branch updated successfully - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: - $ref: '#/definitions/response.Branch' - type: object - "400": - description: Bad request - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: {} - type: object - "401": - description: Unauthorized - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: {} - type: object - summary: Update an existing branch - tags: - - Branch APIs - /api/v1/branch/list: - get: - consumes: - - application/json - description: Get a paginated list of branches based on query parameters. - parameters: - - description: JWT token - in: header - name: Authorization - required: true - type: string - - description: Number of items to retrieve (default 10) - in: query - name: Limit - type: integer - - description: Offset for pagination (default 0) - in: query - name: Offset - type: integer - produces: - - application/json - responses: - "200": - description: List of branches - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: - $ref: '#/definitions/response.BranchList' - type: object - "400": - description: Bad request - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: {} - type: object - "401": - description: Unauthorized - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: {} - type: object - summary: Get a list of branches - tags: - - Branch APIs - /api/v1/file/upload: - post: - consumes: - - multipart/form-data - description: Upload a file to Alibaba Cloud OSS with the provided details. - parameters: - - description: JWT token - in: header - name: Authorization - required: true - type: string - - description: 'File to upload (max size: 2MB)' - in: formData - name: file - required: true - type: file - produces: - - application/json - responses: - "200": - description: File uploaded successfully - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: - $ref: '#/definitions/entity.UploadFileResponse' - type: object - "400": - description: Bad request - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: {} - type: object - "401": - description: Unauthorized - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: {} - type: object - summary: Upload a file to OSS - tags: - - File Upload API - /api/v1/order: - post: - consumes: - - application/json - description: Create a new order with the provided details. - parameters: - - description: JWT token - in: header - name: Authorization - required: true - type: string - - description: Order details - in: body - name: order - required: true - schema: - $ref: '#/definitions/request.Order' - produces: - - application/json - responses: - "200": - description: Order created successfully - schema: - $ref: '#/definitions/response.BaseResponse' - "400": - description: Bad request - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: {} - type: object - "401": - description: Unauthorized - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: {} - type: object - summary: Create a new order - /api/v1/order/{id}: - get: - consumes: - - application/json - description: Retrieve the details of the specified order by ID. - parameters: - - description: JWT token - in: header - name: Authorization - required: true - type: string - - description: Order ID - in: path - name: id - required: true - type: string - produces: - - application/json - responses: - "200": - description: Order details retrieved successfully - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: - $ref: '#/definitions/response.Order' - type: object - "400": - description: Bad request - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: {} - type: object - "401": - description: Unauthorized - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: {} - type: object - "500": - description: Internal server error - schema: - $ref: '#/definitions/response.BaseResponse' - summary: Get details of an order by ID - /api/v1/order/branch-revenue: - get: - consumes: - - application/json - description: Retrieve the branch-wise revenue for orders based on the specified - parameters. - parameters: - - description: JWT token - in: header - name: Authorization - required: true - type: string - - description: Branch ID for filtering - in: query - name: branch_id - type: integer - - description: 'Start date for filtering (format: ''YYYY-MM-DD'')' - in: query - name: start_date - type: string - - description: 'End date for filtering (format: ''YYYY-MM-DD'')' - in: query - name: end_date - type: string - produces: - - application/json - responses: - "200": - description: Branch-wise revenue retrieved successfully - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: - items: - $ref: '#/definitions/response.OrderBranchRevenue' - type: array - type: object - "400": - description: Bad request - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: {} - type: object - "401": - description: Unauthorized - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: {} - type: object - "500": - description: Internal server error - schema: - $ref: '#/definitions/response.BaseResponse' - summary: Get branch-wise revenue for orders - /api/v1/order/list: - get: - consumes: - - application/json - description: Retrieve a list of orders based on the specified parameters. - parameters: - - description: JWT token - in: header - name: Authorization - required: true - type: string - - description: 'Number of items to retrieve (default: 10)' - in: query - name: limit - type: integer - - description: 'Number of items to skip (default: 0)' - in: query - name: offset - type: integer - produces: - - application/json - responses: - "200": - description: List of orders retrieved successfully - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: - $ref: '#/definitions/response.OrderList' - type: object - "400": - description: Bad request - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: {} - type: object - "401": - description: Unauthorized - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: {} - type: object - "500": - description: Internal server error - schema: - $ref: '#/definitions/response.BaseResponse' - summary: Get a list of orders - /api/v1/order/total-revenue: - get: - consumes: - - application/json - description: Retrieve the total revenue and number of transactions for orders - based on the specified parameters. - parameters: - - description: JWT token - in: header - name: Authorization - required: true - type: string - - description: 'Start date for filtering (format: ''YYYY-MM-DD'')' - in: query - name: start_date - type: string - - description: 'End date for filtering (format: ''YYYY-MM-DD'')' - in: query - name: end_date - type: string - produces: - - application/json - responses: - "200": - description: Total revenue and transactions retrieved successfully - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: - $ref: '#/definitions/response.OrderMonthlyRevenue' - type: object - "400": - description: Bad request - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: {} - type: object - "401": - description: Unauthorized - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: {} - type: object - "500": - description: Internal server error - schema: - $ref: '#/definitions/response.BaseResponse' - summary: Get total revenue and number of transactions for orders - /api/v1/order/update-status/{id}: - put: - consumes: - - application/json - description: Update the status of the specified order with the provided details. - parameters: - - description: JWT token - in: header - name: Authorization - required: true - type: string - - description: Order ID - in: path - name: id - required: true - type: string - - description: Status details - in: body - name: status - required: true - schema: - $ref: '#/definitions/request.UpdateStatus' - produces: - - application/json - responses: - "200": - description: Order status updated successfully - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: - $ref: '#/definitions/response.Order' - type: object - "400": - description: Bad request - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: {} - type: object - "401": - description: Unauthorized - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: {} - type: object - "500": - description: Internal server error - schema: - $ref: '#/definitions/response.BaseResponse' - summary: Update the status of an order - /api/v1/order/yearly-revenue/{year}: - get: - consumes: - - application/json - description: Retrieve the yearly revenue for orders based on the specified year. - parameters: - - description: JWT token - in: header - name: Authorization - required: true - type: string - - description: Year for filtering - in: path - name: year - required: true - type: integer - produces: - - application/json - responses: - "200": - description: Yearly revenue retrieved successfully - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: - additionalProperties: - additionalProperties: - type: number - type: object - type: object - type: object - "400": - description: Bad request - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: {} - type: object - "401": - description: Unauthorized - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: {} - type: object - "500": - description: Internal server error - schema: - $ref: '#/definitions/response.BaseResponse' - summary: Get yearly revenue for orders - /api/v1/product/: - post: - consumes: - - application/json - description: Create a new product with the provided details. - parameters: - - description: JWT token - in: header - name: Authorization - required: true - type: string - - description: Product details to create - in: body - name: req - required: true - schema: - $ref: '#/definitions/request.Product' - produces: - - application/json - responses: - "200": - description: Product created successfully - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: - $ref: '#/definitions/response.Product' - type: object - "400": - description: Bad request - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: {} - type: object - "401": - description: Unauthorized - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: {} - type: object - summary: Create a new product - tags: - - Product APIs - /api/v1/product/{id}: - delete: - consumes: - - application/json - description: Delete a product based on the provided ID. - parameters: - - description: JWT token - in: header - name: Authorization - required: true - type: string - - description: Product ID to delete - in: path - name: id - required: true - type: integer - produces: - - application/json - responses: - "200": - description: Product deleted successfully - schema: - $ref: '#/definitions/response.BaseResponse' - "400": - description: Bad request - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: {} - type: object - "401": - description: Unauthorized - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: {} - type: object - summary: Delete a product by ID - tags: - - Product APIs - get: - consumes: - - application/json - description: Get details of a product based on the provided ID. - parameters: - - description: JWT token - in: header - name: Authorization - required: true - type: string - - description: Product ID to retrieve - in: path - name: id - required: true - type: integer - produces: - - application/json - responses: - "200": - description: Product details - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: - $ref: '#/definitions/response.Product' - type: object - "400": - description: Bad request - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: {} - type: object - "401": - description: Unauthorized - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: {} - type: object - summary: Get details of a product by ID - tags: - - Product APIs - put: - consumes: - - application/json - description: Update the details of an existing product based on the provided - ID. - parameters: - - description: JWT token - in: header - name: Authorization - required: true - type: string - - description: Product ID to update - in: path - name: id - required: true - type: integer - - description: Updated product details - in: body - name: req - required: true - schema: - $ref: '#/definitions/request.Product' - produces: - - application/json - responses: - "200": - description: Product updated successfully - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: - $ref: '#/definitions/response.Product' - type: object - "400": - description: Bad request - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: {} - type: object - "401": - description: Unauthorized - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: {} - type: object - summary: Update an existing product - tags: - - Product APIs - /api/v1/product/list: - get: - consumes: - - application/json - description: Get a paginated list of products based on query parameters. - parameters: - - description: JWT token - in: header - name: Authorization - required: true - type: string - - description: Number of items to retrieve (default 10) - in: query - name: Limit - type: integer - - description: Offset for pagination (default 0) - in: query - name: Offset - type: integer - produces: - - application/json - responses: - "200": - description: List of products - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: - $ref: '#/definitions/response.ProductList' - type: object - "400": - description: Bad request - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: {} - type: object - "401": - description: Unauthorized - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: {} - type: object - summary: Get a list of products - tags: - - Product APIs - /api/v1/studio: - post: - consumes: - - application/json - description: Create a new studio based on the provided details. - parameters: - - description: JWT token - in: header - name: Authorization - required: true - type: string - - description: New studio details - in: body - name: req - required: true - schema: - $ref: '#/definitions/request.Studio' - produces: - - application/json - responses: - "200": - description: Studio created successfully - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: - $ref: '#/definitions/response.Studio' - type: object - "400": - description: Bad request - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: {} - type: object - "401": - description: Unauthorized - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: {} - type: object - summary: Create a new studio - tags: - - Studio APIs - /api/v1/studio/{id}: - get: - consumes: - - application/json - description: Get details of a studio based on the provided ID. - parameters: - - description: JWT token - in: header - name: Authorization - required: true - type: string - - description: Studio ID to retrieve - in: path - name: id - required: true - type: integer - produces: - - application/json - responses: - "200": - description: Studio details - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: - $ref: '#/definitions/response.Studio' - type: object - "400": - description: Bad request - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: {} - type: object - "401": - description: Unauthorized - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: {} - type: object - summary: Get details of a studio by ID - tags: - - Studio APIs - put: - consumes: - - application/json - description: Update the details of an existing studio based on the provided - ID. - parameters: - - description: JWT token - in: header - name: Authorization - required: true - type: string - - description: Studio ID to update - in: path - name: id - required: true - type: integer - - description: Updated studio details - in: body - name: req - required: true - schema: - $ref: '#/definitions/request.Studio' - produces: - - application/json - responses: - "200": - description: Studio updated successfully - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: - $ref: '#/definitions/response.Studio' - type: object - "400": - description: Bad request - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: {} - type: object - "401": - description: Unauthorized - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: {} - type: object - summary: Update an existing studio - tags: - - Studio APIs - /api/v1/studio/search: - get: - consumes: - - application/json - description: Search for studios based on query parameters. - parameters: - - description: JWT token - in: header - name: Authorization - required: true - type: string - - description: Studio name for search - in: query - name: Name - type: string - - description: Studio status for search - in: query - name: Status - type: string - - description: Number of items to retrieve (default 10) - in: query - name: Limit - type: integer - - description: Offset for pagination (default 0) - in: query - name: Offset - type: integer - produces: - - application/json - responses: - "200": - description: List of studios - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: - $ref: '#/definitions/response.StudioList' - type: object - "400": - description: Bad request - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: {} - type: object - "401": - description: Unauthorized - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: {} - type: object - summary: Search for studios - tags: - - Studio APIs - /api/v1/user: - post: - consumes: - - application/json - description: Create a new user based on the provided data. - parameters: - - description: JWT token - in: header - name: Authorization - required: true - type: string - - description: New user details - in: body - name: req - required: true - schema: - $ref: '#/definitions/request.User' - produces: - - application/json - responses: - "200": - description: User created successfully - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: - $ref: '#/definitions/response.User' - type: object - "400": - description: Bad request - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: {} - type: object - "401": - description: Unauthorized - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: {} - type: object - summary: Create a new user - tags: - - User APIs - /api/v1/user/{id}: - delete: - consumes: - - application/json - description: Delete a user based on the provided ID. - parameters: - - description: JWT token - in: header - name: Authorization - required: true - type: string - - description: User ID to delete - in: path - name: id - required: true - type: integer - produces: - - application/json - responses: - "200": - description: User deleted successfully - schema: - $ref: '#/definitions/response.BaseResponse' - "400": - description: Bad request - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: {} - type: object - "401": - description: Unauthorized - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: {} - type: object - summary: Delete a user by ID - tags: - - User APIs - get: - consumes: - - application/json - description: Get details of a user based on the provided ID. - parameters: - - description: JWT token - in: header - name: Authorization - required: true - type: string - - description: User ID to retrieve - in: path - name: id - required: true - type: integer - produces: - - application/json - responses: - "200": - description: User details - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: - $ref: '#/definitions/response.User' - type: object - "400": - description: Bad request - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: {} - type: object - "401": - description: Unauthorized - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: {} - type: object - summary: Get details of a user by ID - tags: - - User APIs - put: - consumes: - - application/json - description: Update the details of an existing user based on the provided ID. - parameters: - - description: JWT token - in: header - name: Authorization - required: true - type: string - - description: User ID to update - in: path - name: id - required: true - type: integer - - description: Updated user details - in: body - name: req - required: true - schema: - $ref: '#/definitions/request.User' - produces: - - application/json - responses: - "200": - description: User updated successfully - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: - $ref: '#/definitions/response.User' - type: object - "400": - description: Bad request - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: {} - type: object - "401": - description: Unauthorized - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: {} - type: object - summary: Update an existing user - tags: - - User APIs - /api/v1/user/list: - get: - consumes: - - application/json - description: Get a paginated list of users based on query parameters. - parameters: - - description: JWT token - in: header - name: Authorization - required: true - type: string - - description: Number of items to retrieve (default 10) - in: query - name: Limit - type: integer - - description: Offset for pagination (default 0) - in: query - name: Offset - type: integer - produces: - - application/json - responses: - "200": - description: List of users - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: - $ref: '#/definitions/response.UserList' - type: object - "400": - description: Bad request - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: {} - type: object - "401": - description: Unauthorized - schema: - allOf: - - $ref: '#/definitions/response.BaseResponse' - - properties: - data: {} - type: object - summary: Get a list of users - tags: - - User APIs -swagger: "2.0" diff --git a/go.mod b/go.mod index 39c0739..0604cbf 100644 --- a/go.mod +++ b/go.mod @@ -1,41 +1,28 @@ -module enaklo-pos-be +module apskel-pos-be -go 1.20 +go 1.21 require ( github.com/gin-gonic/gin v1.9.1 github.com/go-playground/validator/v10 v10.17.0 - github.com/gofrs/uuid v4.2.0+incompatible - github.com/golang-jwt/jwt v3.2.2+incompatible github.com/google/uuid v1.1.2 github.com/lib/pq v1.2.0 github.com/spf13/viper v1.16.0 - github.com/swaggo/files v1.0.1 - github.com/swaggo/gin-swagger v1.6.0 - github.com/swaggo/swag v1.16.2 gopkg.in/yaml.v3 v3.0.1 - gorm.io/datatypes v1.2.0 ) require ( - github.com/KyleBanks/depth v1.2.1 // indirect - github.com/antihax/optional v1.0.0 // indirect github.com/bytedance/sonic v1.10.2 // indirect - github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927 // indirect github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect github.com/chenzhuoyu/iasm v0.9.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/gin-contrib/sse v0.1.0 // indirect - github.com/go-openapi/jsonpointer v0.20.2 // indirect - github.com/go-openapi/jsonreference v0.20.4 // indirect - github.com/go-openapi/spec v0.20.14 // indirect - github.com/go-openapi/swag v0.22.8 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-sql-driver/mysql v1.7.0 // indirect github.com/goccy/go-json v0.10.2 // indirect + github.com/google/go-cmp v0.6.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect @@ -43,21 +30,17 @@ require ( github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect - github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.6 // indirect github.com/leodido/go-urn v1.2.4 // indirect github.com/magiconair/properties v1.8.7 // indirect - github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/pelletier/go-toml/v2 v2.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/richardlehane/mscfb v1.0.4 // indirect - github.com/richardlehane/msoleps v1.0.4 // indirect + github.com/rogpeppe/go-internal v1.11.0 // indirect github.com/spf13/afero v1.9.5 // indirect github.com/spf13/cast v1.5.1 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect @@ -66,31 +49,24 @@ require ( github.com/subosito/gotenv v1.4.2 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect - github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d // indirect - github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 // indirect go.uber.org/atomic v1.10.0 // indirect go.uber.org/multierr v1.8.0 // indirect golang.org/x/arch v0.7.0 // indirect golang.org/x/net v0.30.0 // indirect - golang.org/x/oauth2 v0.21.0 // indirect golang.org/x/sys v0.26.0 // indirect golang.org/x/text v0.19.0 // indirect - golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect google.golang.org/protobuf v1.32.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect - gorm.io/driver/mysql v1.4.7 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect ) require ( - github.com/aws/aws-sdk-go v1.50.0 - github.com/getbrevo/brevo-go v1.0.0 - github.com/pkg/errors v0.9.1 + github.com/aws/aws-sdk-go v1.55.7 + github.com/golang-jwt/jwt/v5 v5.2.3 + github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.8.4 - github.com/veritrans/go-midtrans v0.0.0-20210616100512-16326c5eeb00 - github.com/xuri/excelize/v2 v2.9.0 go.uber.org/zap v1.21.0 golang.org/x/crypto v0.28.0 - golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6 gorm.io/driver/postgres v1.5.0 gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11 ) diff --git a/go.sum b/go.sum index b2f5632..2c52301 100644 --- a/go.sum +++ b/go.sum @@ -38,12 +38,8 @@ cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3f dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= -github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= -github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/aws/aws-sdk-go v1.50.0 h1:HBtrLeO+QyDKnc3t1+5DR1RxodOHCGr8ZcrHudpv7jI= -github.com/aws/aws-sdk-go v1.50.0/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= +github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE= +github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= @@ -51,8 +47,6 @@ github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ github.com/bytedance/sonic v1.10.2 h1:GQebETVBxYB7JGWJtLBi07OVzWwt+8dWA00gEVW2ZFE= github.com/bytedance/sonic v1.10.2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927 h1:SKI1/fuSdodxmNNyVBR8d7X/HuLnRpvvFO0AgyQk764= -github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927/go.mod h1:h/aW8ynjgkuj+NQRlZcDbAbM1ORAbXjXX77sX7T289U= github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0= @@ -78,13 +72,11 @@ github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5y github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= +github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= -github.com/getbrevo/brevo-go v1.0.0 h1:E/pRCsQeExvZeTCJU5vy+xHWcLaL5axWQ9QkxjlFke4= -github.com/getbrevo/brevo-go v1.0.0/go.mod h1:2TBMEnaDqq/oiAXUYtn6eykiEdHcEoS7tc63+YoFibw= -github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= @@ -92,31 +84,18 @@ github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SU github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-openapi/jsonpointer v0.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q= -github.com/go-openapi/jsonpointer v0.20.2/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs= -github.com/go-openapi/jsonreference v0.20.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdXSSgNeAhojU= -github.com/go-openapi/jsonreference v0.20.4/go.mod h1:5pZJyJP2MnYCpoeoMAql78cCHauHj0V9Lhc506VOpw4= -github.com/go-openapi/spec v0.20.14 h1:7CBlRnw+mtjFGlPDRZmAMnq35cRzI91xj03HVyUi/Do= -github.com/go-openapi/spec v0.20.14/go.mod h1:8EOhTpBoFiask8rrgwbLC3zmJfz4zsCUueRuPM6GNkw= -github.com/go-openapi/swag v0.22.8 h1:/9RjDSQ0vbFR+NyjGMkFTsA1IA0fmhKSThmfGZjicbw= -github.com/go-openapi/swag v0.22.8/go.mod h1:6QT22icPLEqAM/z/TChgb4WAveCHF92+2gF0CNjHpPI= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.17.0 h1:SmVVlfAOtlZncTxRuinDPomC2DkXJ4E5T9gDA0AIH74= github.com/go-playground/validator/v10 v10.17.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= -github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= -github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/gofrs/uuid v4.2.0+incompatible h1:yyYWMnhkhrKwwr8gAOcOCYxOOscHgDS9yZgBrnJfGa0= -github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= -github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= -github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= -github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= -github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= +github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0= +github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -154,6 +133,7 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -189,15 +169,12 @@ github.com/jackc/pgx/v5 v5.3.0/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHo github.com/jackc/puddle/v2 v2.2.0/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= -github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= -github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= @@ -212,6 +189,7 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -222,12 +200,8 @@ github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI= -github.com/microsoft/go-mssqldb v0.17.0 h1:Fto83dMZPnYv1Zwx5vHHxpNraeEaUlQ/hhHLgZiaenE= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -235,8 +209,6 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= -github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI= github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -246,14 +218,12 @@ github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qR github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM= -github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk= -github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= -github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00= -github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= @@ -281,24 +251,10 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= -github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= -github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= -github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M= -github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo= -github.com/swaggo/swag v1.16.2 h1:28Pp+8DkQoV+HLzLx8RGJZXNGKbFqnuvSbAAtoxiY04= -github.com/swaggo/swag v1.16.2/go.mod h1:6YzXnDcpr0767iOejs318CwYkCQqyGer6BizOg03f+E= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= -github.com/veritrans/go-midtrans v0.0.0-20210616100512-16326c5eeb00 h1:iCcVFY2mUdalvtpNN0M/vcf7+OYHGKXwzG5JLZgjwQU= -github.com/veritrans/go-midtrans v0.0.0-20210616100512-16326c5eeb00/go.mod h1:21mwYsDK+z+5kR2fvUB8n2yijZZm504Vjzk1s0rNQJg= -github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d h1:llb0neMWDQe87IzJLS4Ci7psK/lVsjIS2otl+1WyRyY= -github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= -github.com/xuri/excelize/v2 v2.9.0 h1:1tgOaEq92IOEumR1/JfYS/eR0KHOCsRv/rYXXh6YJQE= -github.com/xuri/excelize/v2 v2.9.0/go.mod h1:uqey4QBZ9gdMeWApPLdhm9x+9o2lq4iVmjiLfBS5hdE= -github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 h1:hPVCafDV85blFTabnqKgNhDCkJX25eik94Si9cTER4A= -github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -344,11 +300,9 @@ golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6 h1:QE6XYQK6naiK1EPAe1g/ILLxN5RBoH5xkJk3CqlMI/Y= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -372,7 +326,6 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -408,7 +361,6 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -420,8 +372,6 @@ golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= -golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -435,7 +385,6 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -474,6 +423,7 @@ golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -546,8 +496,6 @@ golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -652,19 +600,13 @@ gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gorm.io/datatypes v1.2.0 h1:5YT+eokWdIxhJgWHdrb2zYUimyk0+TaFth+7a0ybzco= -gorm.io/datatypes v1.2.0/go.mod h1:o1dh0ZvjIjhH/bngTpypG6lVRJ5chTBxE09FH/71k04= -gorm.io/driver/mysql v1.4.7 h1:rY46lkCspzGHn7+IYsNpSfEv9tA+SU4SkkB+GFX125Y= -gorm.io/driver/mysql v1.4.7/go.mod h1:SxzItlnT1cb6e1e4ZRpgJN2VYtcqJgqnHxWr4wsP8oc= gorm.io/driver/postgres v1.5.0 h1:u2FXTy14l45qc3UeCJ7QaAXZmZfDDv0YrthvmRq1l0U= gorm.io/driver/postgres v1.5.0/go.mod h1:FUZXzO+5Uqg5zzwzv4KK49R8lvGIyscBOqYrtI1Ce9A= -gorm.io/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU= -gorm.io/driver/sqlserver v1.4.1 h1:t4r4r6Jam5E6ejqP7N82qAJIJAht27EGT41HyPfXRw0= -gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11 h1:9qNbmu21nNThCNnF5i2R3kw2aL27U8ZwbzccNjOmW0g= gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/infra/development.yaml b/infra/development.yaml new file mode 100644 index 0000000..788cb91 --- /dev/null +++ b/infra/development.yaml @@ -0,0 +1,34 @@ +server: + base-url: + local-url: + port: 4000 + +jwt: + token: + expires-ttl: 1440 + secret: "5Lm25V3Qd7aut8dr4QUxm5PZUrSFs" + +postgresql: + host: 62.72.45.250 + port: 5433 + driver: postgres + db: apskel_pos + username: apskel + password: '7a8UJbM2GgBWaseh0lnP3O5i1i5nINXk' + ssl-mode: disable + max-idle-connections-in-second: 600 + max-open-connections-in-second: 600 + connection-max-life-time-in-second: 600 + debug: false + +s3: + access_key_id: cf9a475e18bc7626cbdbf09709d82a64 + access_key_secret: 91f3321294d3e23035427a0ecb893ada + endpoint: sin1.contabostorage.com + bucket_name: enaklo + log_level: Error + host_url: 'https://sin1.contabostorage.com/fda98c2228f246f29a7e466b86b3b9e7:' + +log: + log_format: 'json' + log_level: 'debug' \ No newline at end of file diff --git a/infra/enaklopos.development.yaml b/infra/enaklopos.development.yaml deleted file mode 100644 index 599676a..0000000 --- a/infra/enaklopos.development.yaml +++ /dev/null @@ -1,96 +0,0 @@ -server: - base-url: https://api.enaklo-pos.id/core - local-url: http://localhost:3300 - port: 3300 - -jwt: - token: - expires-ttl: 1440 - secret: "5Lm25V3Qd7aut8dr4QUxm5PZUrSFs" - token-order: - expires-ttl: 2 - secret: "123Lm25V3Qd7aut8dr4QUxm5PZUrSFs" - token-withdraw: - expires-ttl: 2 - secret: "909Lm25V3Qd7aut8dr4QUxm5PZUrSFs" - token-customer: - expires-ttl: 1400 - secret: "WakLm25V3Qd7aut8dr4QUxm5PZUrWa#" - -postgresql: - host: 62.72.45.250 - port: 22010 - driver: postgres - db: enaklo-pos-staging - username: admin - password: '4W^he3^BBBmPisWa$J#2' - ssl-mode: disable - max-idle-connections-in-second: 600 - max-open-connections-in-second: 600 - connection-max-life-time-in-second: 600 - debug: false - -oss: - access_key_id: cf9a475e18bc7626cbdbf09709d82a64 - access_key_secret: 91f3321294d3e23035427a0ecb893ada - endpoint: sin1.contabostorage.com - bucket_name: enaklo - log_level: Error - host_url: 'https://sin1.contabostorage.com/fda98c2228f246f29a7e466b86b3b9e7:' - -midtrans: - server_key: "SB-Mid-server-YOIvuaIlRw3In9SymCuFz-hB" - client_key: "SB-Mid-client-ulkZGFiS8PqBNOZz" - env: 1 - -linkqu: - base_url: "https://gateway-dev.linkqu.id" - client_id: "testing" - client_secret: "123" - signature_key: "LinkQu@2020" - username: "LI307GXIN" - pin: "2K2NPCBBNNTovgB" - callback_url: "https://enaklo-pos-be.app-dev.altru.id/api/v1/linkqu/callback" - -brevo: - api_key: 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 - -email: - sender: "noreply@enaklo.co.id" - sender_customer: "enaklo-pos.official@gmail.com" - reset_password: - template_name: "reset_password" - template_path: "templates/reset_password.html" - template_path_customer: "templates/reset_password_customer.html" - subject: "Reset Password" - opening_word: "Terima kasih sudah menjadi bagian dari enaklo-pos. Anda telah berhasil melakukan reset password, silakan masukan unik password yang dibuat oleh sistem dibawah ini:" - closing_word: "Silakan login kembali menggunakan email dan password anda diatas, sistem akan secara otomatis meminta anda untuk membuat password baru setelah berhasil login. Mohon maaf atas kendala yang dialami." - -order: - fee: 5000 - -withdrawal: - platform_fee: 5000 - -discovery: - explore_destinations: - - name: "Jakarta" - image_url: "https://obs.eranyacloud.com/enaklo-pos-dev/file/03c0b046-43ab-4d35-a743-6a173bc66b90-1722680749.png" - - name: "Banten" - image_url: "https://obs.eranyacloud.com/enaklo-pos-dev/file/c8e7dd8a-17be-449f-afdc-0c07eda438ce-1722680809.png" - - name: "Yogyakarta" - image_url: "https://obs.eranyacloud.com/enaklo-pos-dev/file/83b78c19-4c97-48c9-bc97-a7403e1c4eed-1722680828.png" - - name: "Jawa Barat" - image_url: "https://obs.eranyacloud.com/enaklo-pos-dev/file/07c35ab1-3e20-4858-8d7d-b29517239dc3-1722680848.png" - - name: "Jawa Tengah" - image_url: "https://obs.eranyacloud.com/enaklo-pos-dev/file/a1915a98-c2aa-4997-8e75-bd4e43789b0c-1722680874.png" - - name: "Jawa Timur" - image_url: "https://obs.eranyacloud.com/enaklo-pos-dev/file/7b5d2b86-e8a8-4703-a153-c186021cf088-1722680894.png" - explore_regions: - - name: "Jawa" - - name: "Sumatera" - - name: "Kalimantan" - - name: "Sulawesi" - -feature_toggle: - logger_enabled: false \ No newline at end of file diff --git a/internal/README.md b/internal/README.md new file mode 100644 index 0000000..2ab1edf --- /dev/null +++ b/internal/README.md @@ -0,0 +1,242 @@ +# Internal Architecture + +This document describes the clean architecture implementation for the POS backend with complete separation of concerns between database entities, business models, and constants. + +## 📁 Package Structure + +### `/constants` - Business Constants +- **Purpose**: All business logic constants, enums, and validation helpers +- **Usage**: Used by models, services, and validation layers +- **Features**: + - Type-safe enums (UserRole, OrderStatus, PaymentStatus, etc.) + - Business validation functions (IsValidUserRole, etc.) + - Default values and limits + - No dependencies on database or frameworks + +### `/entities` - Database Models +- **Purpose**: Database-specific models with GORM tags and hooks +- **Usage**: **ONLY** used by repository layer for database operations +- **Features**: + - GORM annotations (`gorm:` tags) + - Database relationships and constraints + - BeforeCreate/AfterCreate hooks + - Table name specifications + - SQL-specific data types + - **Never used in business logic** + +### `/models` - Business Models +- **Purpose**: **Pure** business domain models without any framework dependencies +- **Usage**: Used by services, handlers, and business logic +- **Features**: + - Clean JSON serialization (`json:` tags) + - Validation rules (`validate:` tags) + - Request/Response DTOs + - **Zero GORM dependencies** + - **Zero database annotations** + - Uses constants package for type safety + - Pure business logic methods + +### `/mappers` - Data Transformation +- **Purpose**: Convert between entities and business models +- **Usage**: Bridge between repository and service layers +- **Features**: + - Entity ↔ Model conversion functions + - Request DTO → Entity conversion + - Entity → Response DTO conversion + - Null-safe conversions + - Slice/collection conversions + - Type conversions between constants and entities + +### `/repository` - Data Access Layer +- **Purpose**: Database operations using entities exclusively +- **Usage**: Only works with database entities +- **Features**: + - CRUD operations with entities + - Query methods with entities + - **Private repository implementations** + - Interface-based contracts + - **Never references business models** + +## 🔄 Data Flow + +``` +API Request (JSON) + ↓ +Request DTO (models) + ↓ +Business Logic (services with models + constants) + ↓ +Entity (via mapper) + ↓ +Repository Layer (entities only) + ↓ +Database + ↓ +Entity (from database) + ↓ +Business Model (via mapper) + ↓ +Response DTO (models) + ↓ +API Response (JSON) +``` + +## 🎯 Key Design Principles + +### ✅ **Clean Business Models** +```go + +type User struct { + ID uuid.UUID `json:"id"` + Role constants.UserRole `json:"role"` + } +``` + +```go + +type User struct { + ID uuid.UUID `gorm:"primaryKey" json:"id"` + Role string `gorm:"size:50" json:"role"` + } +``` + +### ✅ **Type-Safe Constants** +```go + +type UserRole string +const ( + RoleAdmin UserRole = "admin" + ) +func IsValidUserRole(role UserRole) bool { /* ... */ } +``` + +```go + +const AdminRole = "admin" ``` + +### ✅ **Repository Isolation** +```go + +func (r *userRepository) Create(ctx context.Context, user *entities.User) error { + return r.db.Create(user).Error +} +``` + +```go + +func (r *userRepository) Create(ctx context.Context, user *models.User) error { + } +``` + +## 📊 Example Usage + +### Service Layer (Business Logic) +```go +func (s *userService) CreateUser(req *models.UserCreateRequest) (*models.UserResponse, error) { + if !constants.IsValidUserRole(req.Role) { + return nil, errors.New("invalid role") + } + + entity := mappers.UserCreateRequestToEntity(req, hashedPassword) + + err := s.userRepo.Create(ctx, entity) + if err != nil { + return nil, err + } + + return mappers.UserEntityToResponse(entity), nil +} +``` + +### Repository Layer (Data Access) +```go +func (r *userRepository) Create(ctx context.Context, user *entities.User) error { + return r.db.WithContext(ctx).Create(user).Error +} +``` + +### Handler Layer (API) +```go +func (h *userHandler) CreateUser(c *gin.Context) { + var req models.UserCreateRequest + + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(400, gin.H{"error": err.Error()}) + return + } + + resp, err := h.userService.CreateUser(&req) + if err != nil { + c.JSON(500, gin.H{"error": err.Error()}) + return + } + + c.JSON(201, resp) +} +``` + +## 🏗️ Architecture Benefits + +1. **🎯 Single Responsibility**: Each package has one clear purpose +2. **🔒 Zero Database Leakage**: Business logic never sees database concerns +3. **🧪 Testability**: Easy to mock interfaces and test business logic +4. **🔧 Maintainability**: Changes to database don't affect business models +5. **🚀 Flexibility**: Can change ORM without touching business logic +6. **📜 API Stability**: Business models provide stable contracts +7. **🛡️ Type Safety**: Constants package prevents invalid states +8. **🧹 Clean Code**: No mixed concerns anywhere in the codebase + +## 📋 Development Guidelines + +### Constants Package (`/constants`) +- ✅ Define all business enums and constants +- ✅ Provide validation helper functions +- ✅ Include default values and limits +- ❌ Never import database or framework packages +- ❌ No business logic, only constants and validation + +### Models Package (`/models`) +- ✅ Pure business structs with JSON tags only +- ✅ Use constants package for type safety +- ✅ Include validation tags for input validation +- ✅ Separate Request/Response DTOs +- ✅ Add business logic methods (validation, calculations) +- ❌ **NEVER** include GORM tags or database annotations +- ❌ **NEVER** import database packages +- ❌ No database relationships or foreign keys + +### Entities Package (`/entities`) +- ✅ Include GORM tags and database constraints +- ✅ Define relationships and foreign keys +- ✅ Add database hooks (BeforeCreate, etc.) +- ✅ Use database-specific types +- ❌ **NEVER** use in business logic or handlers +- ❌ **NEVER** add business validation rules + +### Mappers Package (`/mappers`) +- ✅ Always check for nil inputs +- ✅ Handle type conversions between constants and strings +- ✅ Provide slice conversion helpers +- ✅ Keep conversions simple and direct +- ❌ No business logic in mappers +- ❌ No database operations + +### Repository Package (`/repository`) +- ✅ Work exclusively with entities +- ✅ Use private repository implementations +- ✅ Provide clean interface contracts +- ❌ **NEVER** reference business models +- ❌ **NEVER** import models package + +## 🚀 Migration Complete + +**All packages have been successfully reorganized:** + +- ✅ **4 Constants files** - All business constants moved to type-safe enums +- ✅ **10 Clean Model files** - Zero GORM dependencies, pure business logic +- ✅ **11 Entity files** - Database-only models with GORM tags +- ✅ **11 Repository files** - Updated to use entities exclusively +- ✅ **2 Mapper files** - Handle conversions between layers +- ✅ **Complete separation** - No cross-layer dependencies + +**The codebase now follows strict clean architecture principles with complete separation of database concerns from business logic!** 🎉 \ No newline at end of file diff --git a/internal/app/app.go b/internal/app/app.go new file mode 100644 index 0000000..d8b7fcc --- /dev/null +++ b/internal/app/app.go @@ -0,0 +1,284 @@ +package app + +import ( + "apskel-pos-be/internal/client" + "context" + "log" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "apskel-pos-be/config" + "apskel-pos-be/internal/handler" + "apskel-pos-be/internal/middleware" + "apskel-pos-be/internal/processor" + "apskel-pos-be/internal/repository" + "apskel-pos-be/internal/router" + "apskel-pos-be/internal/service" + "apskel-pos-be/internal/validator" + + "gorm.io/gorm" +) + +type App struct { + server *http.Server + db *gorm.DB + router *router.Router + shutdown chan os.Signal +} + +func NewApp(db *gorm.DB) *App { + return &App{ + db: db, + shutdown: make(chan os.Signal, 1), + } +} + +func (a *App) Initialize(cfg *config.Config) error { + repos := a.initRepositories() + processors := a.initProcessors(cfg, repos) + services := a.initServices(processors, cfg) + validators := a.initValidators() + middleware := a.initMiddleware(services) + healthHandler := handler.NewHealthHandler() + + a.router = router.NewRouter( + cfg, + healthHandler, + services.authService, + middleware.authMiddleware, + services.userService, + validators.userValidator, + services.organizationService, + validators.organizationValidator, + services.outletService, + validators.outletValidator, + services.outletSettingService, + services.categoryService, + validators.categoryValidator, + services.productService, + validators.productValidator, + services.productVariantService, + validators.productVariantValidator, + services.inventoryService, + validators.inventoryValidator, + services.orderService, + validators.orderValidator, + services.fileService, + validators.fileValidator, + services.customerService, + validators.customerValidator, + services.paymentMethodService, + validators.paymentMethodValidator, + services.analyticsService, + ) + + return nil +} + +func (a *App) Start(port string) error { + engine := a.router.Init() + + a.server = &http.Server{ + Addr: ":" + port, + Handler: engine, + ReadTimeout: 15 * time.Second, + WriteTimeout: 15 * time.Second, + IdleTimeout: 60 * time.Second, + } + + signal.Notify(a.shutdown, os.Interrupt, syscall.SIGTERM) + + go func() { + log.Printf("Server starting on port %s", port) + if err := a.server.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Fatalf("Failed to start server: %v", err) + } + }() + + <-a.shutdown + log.Println("Shutting down server...") + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + if err := a.server.Shutdown(ctx); err != nil { + log.Printf("Server forced to shutdown: %v", err) + return err + } + + log.Println("Server exited gracefully") + return nil +} + +func (a *App) Shutdown() { + close(a.shutdown) +} + +type repositories struct { + userRepo *repository.UserRepositoryImpl + organizationRepo *repository.OrganizationRepositoryImpl + outletRepo *repository.OutletRepositoryImpl + outletSettingRepo *repository.OutletSettingRepositoryImpl + categoryRepo *repository.CategoryRepositoryImpl + productRepo *repository.ProductRepositoryImpl + productVariantRepo *repository.ProductVariantRepositoryImpl + inventoryRepo *repository.InventoryRepositoryImpl + orderRepo *repository.OrderRepositoryImpl + orderItemRepo *repository.OrderItemRepositoryImpl + paymentRepo *repository.PaymentRepositoryImpl + paymentMethodRepo *repository.PaymentMethodRepositoryImpl + fileRepo *repository.FileRepositoryImpl + customerRepo *repository.CustomerRepository + analyticsRepo *repository.AnalyticsRepositoryImpl +} + +func (a *App) initRepositories() *repositories { + return &repositories{ + userRepo: repository.NewUserRepository(a.db), + organizationRepo: repository.NewOrganizationRepositoryImpl(a.db), + outletRepo: repository.NewOutletRepositoryImpl(a.db), + outletSettingRepo: repository.NewOutletSettingRepositoryImpl(a.db), + categoryRepo: repository.NewCategoryRepositoryImpl(a.db), + productRepo: repository.NewProductRepositoryImpl(a.db), + productVariantRepo: repository.NewProductVariantRepositoryImpl(a.db), + inventoryRepo: repository.NewInventoryRepositoryImpl(a.db), + orderRepo: repository.NewOrderRepositoryImpl(a.db), + orderItemRepo: repository.NewOrderItemRepositoryImpl(a.db), + paymentRepo: repository.NewPaymentRepositoryImpl(a.db), + paymentMethodRepo: repository.NewPaymentMethodRepositoryImpl(a.db), + fileRepo: repository.NewFileRepositoryImpl(a.db), + customerRepo: repository.NewCustomerRepository(a.db), + analyticsRepo: repository.NewAnalyticsRepositoryImpl(a.db), + } +} + +type processors struct { + userProcessor *processor.UserProcessorImpl + organizationProcessor processor.OrganizationProcessor + outletProcessor processor.OutletProcessor + outletSettingProcessor *processor.OutletSettingProcessorImpl + categoryProcessor processor.CategoryProcessor + productProcessor processor.ProductProcessor + productVariantProcessor processor.ProductVariantProcessor + inventoryProcessor processor.InventoryProcessor + orderProcessor processor.OrderProcessor + paymentMethodProcessor processor.PaymentMethodProcessor + fileProcessor processor.FileProcessor + customerProcessor *processor.CustomerProcessor + analyticsProcessor *processor.AnalyticsProcessorImpl +} + +func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processors { + fileClient := client.NewFileClient(cfg.S3Config) + + return &processors{ + userProcessor: processor.NewUserProcessor(repos.userRepo, repos.organizationRepo, repos.outletRepo), + organizationProcessor: processor.NewOrganizationProcessorImpl(repos.organizationRepo, repos.outletRepo, repos.userRepo), + outletProcessor: processor.NewOutletProcessorImpl(repos.outletRepo), + outletSettingProcessor: processor.NewOutletSettingProcessorImpl(repos.outletSettingRepo, repos.outletRepo), + categoryProcessor: processor.NewCategoryProcessorImpl(repos.categoryRepo), + productProcessor: processor.NewProductProcessorImpl(repos.productRepo, repos.categoryRepo, repos.productVariantRepo, repos.inventoryRepo, repos.outletRepo), + productVariantProcessor: processor.NewProductVariantProcessorImpl(repos.productVariantRepo, repos.productRepo), + inventoryProcessor: processor.NewInventoryProcessorImpl(repos.inventoryRepo, repos.productRepo, repos.outletRepo), + orderProcessor: processor.NewOrderProcessorImpl(repos.orderRepo, repos.orderItemRepo, repos.paymentRepo, repos.productRepo, repos.paymentMethodRepo, repos.inventoryRepo, repos.productVariantRepo, repos.outletRepo, repos.customerRepo), + paymentMethodProcessor: processor.NewPaymentMethodProcessorImpl(repos.paymentMethodRepo), + fileProcessor: processor.NewFileProcessorImpl(repos.fileRepo, fileClient), + customerProcessor: processor.NewCustomerProcessor(repos.customerRepo), + analyticsProcessor: processor.NewAnalyticsProcessorImpl(repos.analyticsRepo), + } +} + +type services struct { + userService *service.UserServiceImpl + authService service.AuthService + organizationService service.OrganizationService + outletService service.OutletService + outletSettingService service.OutletSettingService + categoryService service.CategoryService + productService service.ProductService + productVariantService service.ProductVariantService + inventoryService service.InventoryService + orderService service.OrderService + paymentMethodService service.PaymentMethodService + fileService service.FileService + customerService service.CustomerService + analyticsService *service.AnalyticsServiceImpl +} + +func (a *App) initServices(processors *processors, cfg *config.Config) *services { + authConfig := cfg.Auth() + jwtSecret := authConfig.AccessTokenSecret() + authService := service.NewAuthService(processors.userProcessor, jwtSecret) + organizationService := service.NewOrganizationService(processors.organizationProcessor) + outletService := service.NewOutletService(processors.outletProcessor) + outletSettingService := service.NewOutletSettingService(processors.outletSettingProcessor) + categoryService := service.NewCategoryService(processors.categoryProcessor) + productService := service.NewProductService(processors.productProcessor) + productVariantService := service.NewProductVariantService(processors.productVariantProcessor) + inventoryService := service.NewInventoryService(processors.inventoryProcessor) + orderService := service.NewOrderServiceImpl(processors.orderProcessor) + paymentMethodService := service.NewPaymentMethodService(processors.paymentMethodProcessor) + fileService := service.NewFileServiceImpl(processors.fileProcessor) + var customerService service.CustomerService = service.NewCustomerService(processors.customerProcessor) + analyticsService := service.NewAnalyticsServiceImpl(processors.analyticsProcessor) + + return &services{ + userService: service.NewUserService(processors.userProcessor), + authService: authService, + organizationService: organizationService, + outletService: outletService, + outletSettingService: outletSettingService, + categoryService: categoryService, + productService: productService, + productVariantService: productVariantService, + inventoryService: inventoryService, + orderService: orderService, + paymentMethodService: paymentMethodService, + fileService: fileService, + customerService: customerService, + analyticsService: analyticsService, + } +} + +type middlewares struct { + authMiddleware *middleware.AuthMiddleware +} + +func (a *App) initMiddleware(services *services) *middlewares { + return &middlewares{ + authMiddleware: middleware.NewAuthMiddleware(services.authService), + } +} + +type validators struct { + userValidator *validator.UserValidatorImpl + organizationValidator validator.OrganizationValidator + outletValidator validator.OutletValidator + categoryValidator validator.CategoryValidator + productValidator validator.ProductValidator + productVariantValidator validator.ProductVariantValidator + inventoryValidator validator.InventoryValidator + orderValidator validator.OrderValidator + paymentMethodValidator validator.PaymentMethodValidator + fileValidator validator.FileValidator + customerValidator validator.CustomerValidator +} + +func (a *App) initValidators() *validators { + return &validators{ + userValidator: validator.NewUserValidator(), + organizationValidator: validator.NewOrganizationValidator(), + outletValidator: validator.NewOutletValidator(), + categoryValidator: validator.NewCategoryValidator(), + productValidator: validator.NewProductValidator(), + productVariantValidator: validator.NewProductVariantValidator(), + inventoryValidator: validator.NewInventoryValidator(), + orderValidator: validator.NewOrderValidator(), + paymentMethodValidator: validator.NewPaymentMethodValidator(), + fileValidator: validator.NewFileValidatorImpl(), + customerValidator: validator.NewCustomerValidator(), + } +} diff --git a/internal/app/server.go b/internal/app/server.go index 225c8a1..5017f1a 100644 --- a/internal/app/server.go +++ b/internal/app/server.go @@ -1,46 +1,18 @@ package app import ( - "enaklo-pos-be/config" "fmt" "github.com/gin-gonic/gin" - "github.com/gofrs/uuid" - - "enaklo-pos-be/internal/middlewares" + "github.com/google/uuid" ) -func NewServer(cfg *config.Config) *Server { - gin.SetMode(gin.ReleaseMode) - - engine := gin.New() - engine.RedirectTrailingSlash = true - engine.RedirectFixedPath = true - - server := &Server{ - engine, - } - - server.Use(middlewares.Cors()) - server.Use(middlewares.LogCorsError()) - server.Use(middlewares.Trace()) - server.Use(middlewares.Logger(&cfg.FeatureToggle)) - server.Use(middlewares.RequestMiddleware(&cfg.FeatureToggle)) - - return server -} - type Server struct { *gin.Engine } -func (*Server) GenerateUUID() (string, error) { - id, err := uuid.NewV4() - if err != nil { - return "", err - } - - return id.String(), nil +func generateServerID() string { + return uuid.New().String() } func (s Server) Listen(address string) error { diff --git a/internal/appcontext/context.go b/internal/appcontext/context.go new file mode 100644 index 0000000..dd35443 --- /dev/null +++ b/internal/appcontext/context.go @@ -0,0 +1,80 @@ +package appcontext + +import ( + "context" + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +type key string + +const ( + CorrelationIDKey = key("CorrelationID") + OrganizationIDKey = key("OrganizationIDKey") + UserIDKey = key("UserID") + OutletIDKey = key("OutletID") + RoleIDKey = key("RoleID") + AppVersionKey = key("AppVersion") + AppIDKey = key("AppID") + AppTypeKey = key("AppType") + PlatformKey = key("platform") + DeviceOSKey = key("deviceOS") + UserLocaleKey = key("userLocale") + UserRoleKey = key("userRole") +) + +func LogFields(ctx interface{}) map[string]interface{} { + fields := make(map[string]interface{}) + fields[string(CorrelationIDKey)] = value(ctx, CorrelationIDKey) + fields[string(OrganizationIDKey)] = value(ctx, OrganizationIDKey) + fields[string(OutletIDKey)] = value(ctx, OutletIDKey) + fields[string(AppVersionKey)] = value(ctx, AppVersionKey) + fields[string(AppIDKey)] = value(ctx, AppIDKey) + fields[string(AppTypeKey)] = value(ctx, AppTypeKey) + fields[string(UserIDKey)] = value(ctx, UserIDKey) + fields[string(PlatformKey)] = value(ctx, PlatformKey) + fields[string(DeviceOSKey)] = value(ctx, DeviceOSKey) + fields[string(UserLocaleKey)] = value(ctx, UserLocaleKey) + return fields +} + +func value(ctx interface{}, key key) string { + switch c := ctx.(type) { + case *gin.Context: + return getFromGinContext(c, key) + case context.Context: + return getFromGoContext(c, key) + default: + return "" + } +} + +func uuidValue(ctx interface{}, key key) uuid.UUID { + switch c := ctx.(type) { + case *gin.Context: + val, _ := uuid.Parse(getFromGinContext(c, key)) + return val + case context.Context: + val, _ := uuid.Parse(getFromGoContext(c, key)) + return val + default: + return uuid.New() + } +} + +func getFromGinContext(c *gin.Context, key key) string { + keyStr := string(key) + if val, exists := c.Get(keyStr); exists { + if str, ok := val.(string); ok { + return str + } + } + return getFromGoContext(c.Request.Context(), key) +} + +func getFromGoContext(ctx context.Context, key key) string { + if val, ok := ctx.Value(key).(string); ok { + return val + } + return "" +} diff --git a/internal/appcontext/context_info.go b/internal/appcontext/context_info.go new file mode 100644 index 0000000..7261b74 --- /dev/null +++ b/internal/appcontext/context_info.go @@ -0,0 +1,81 @@ +package appcontext + +import ( + "context" + "github.com/google/uuid" + "github.com/sirupsen/logrus" +) + +type logCtxKeyType struct{} + +var logCtxKey = logCtxKeyType(struct{}{}) + +type Logger struct { + *logrus.Logger +} + +var log *Logger + +type ContextInfo struct { + CorrelationID string + UserID uuid.UUID + OrganizationID uuid.UUID + OutletID string + AppVersion string + AppID string + AppType string + Platform string + DeviceOS string + UserLocale string + UserRole string +} + +type ctxKeyType struct{} + +var ctxKey = ctxKeyType(struct{}{}) + +func NewAppContext(ctx context.Context, info *ContextInfo) context.Context { + ctx = NewContext(ctx, map[string]interface{}{ + "correlation_id": info.CorrelationID, + "user_id": info.UserID, + "app_version": info.AppVersion, + "app_id": info.AppID, + "app_type": info.AppType, + "platform": info.Platform, + "device_os": info.DeviceOS, + "user_locale": info.UserLocale, + }) + return context.WithValue(ctx, ctxKey, info) +} + +func NewContext(ctx context.Context, baseFields map[string]interface{}) context.Context { + entry, ok := ctx.Value(logCtxKey).(*logrus.Entry) + if !ok { + entry = log.WithFields(map[string]interface{}{}) + } + + return context.WithValue(ctx, logCtxKey, entry.WithFields(baseFields)) +} + +func FromGinContext(ctx context.Context) *ContextInfo { + return &ContextInfo{ + CorrelationID: value(ctx, CorrelationIDKey), + UserID: uuidValue(ctx, UserIDKey), + OutletID: value(ctx, OutletIDKey), + OrganizationID: uuidValue(ctx, OrganizationIDKey), + AppVersion: value(ctx, AppVersionKey), + AppID: value(ctx, AppIDKey), + AppType: value(ctx, AppTypeKey), + Platform: value(ctx, PlatformKey), + DeviceOS: value(ctx, DeviceOSKey), + UserLocale: value(ctx, UserLocaleKey), + UserRole: value(ctx, UserRoleKey), + } +} + +func FromContext(ctx context.Context) *ContextInfo { + if info, ok := ctx.Value(ctxKey).(*ContextInfo); ok { + return info + } + return nil +} diff --git a/internal/repository/oss/oss.go b/internal/client/s3_flie_client.go similarity index 64% rename from internal/repository/oss/oss.go rename to internal/client/s3_flie_client.go index e82d39f..ec81c22 100644 --- a/internal/repository/oss/oss.go +++ b/internal/client/s3_flie_client.go @@ -1,4 +1,4 @@ -package oss +package client import ( "bytes" @@ -11,7 +11,7 @@ import ( "github.com/aws/aws-sdk-go/service/s3" ) -type OSSConfig interface { +type FileConfig interface { GetAccessKeyID() string GetAccessKeySecret() string GetEndpoint() string @@ -22,17 +22,17 @@ type OSSConfig interface { const _awsRegion = "us-east-1" const _s3ACL = "public-read" -type OssRepositoryImpl struct { +type S3FileClientImpl struct { s3 *s3.S3 - cfg OSSConfig + cfg FileConfig } -func NewOssRepositoryImpl(ossCfg OSSConfig) *OssRepositoryImpl { +func NewFileClient(fileCfg FileConfig) *S3FileClientImpl { sess, err := session.NewSession(&aws.Config{ S3ForcePathStyle: aws.Bool(true), - Endpoint: aws.String(ossCfg.GetEndpoint()), + Endpoint: aws.String(fileCfg.GetEndpoint()), Region: aws.String(_awsRegion), - Credentials: credentials.NewStaticCredentials(ossCfg.GetAccessKeyID(), ossCfg.GetAccessKeySecret(), ""), + Credentials: credentials.NewStaticCredentials(fileCfg.GetAccessKeyID(), fileCfg.GetAccessKeySecret(), ""), }) if err != nil { @@ -40,13 +40,13 @@ func NewOssRepositoryImpl(ossCfg OSSConfig) *OssRepositoryImpl { return nil } - return &OssRepositoryImpl{ + return &S3FileClientImpl{ s3: s3.New(sess), - cfg: ossCfg, + cfg: fileCfg, } } -func (r *OssRepositoryImpl) UploadFile(ctx context.Context, fileName string, fileContent []byte) (fileUrl string, err error) { +func (r *S3FileClientImpl) UploadFile(ctx context.Context, fileName string, fileContent []byte) (fileUrl string, err error) { reader := bytes.NewReader(fileContent) _, err = r.s3.PutObject(&s3.PutObjectInput{ @@ -59,7 +59,7 @@ func (r *OssRepositoryImpl) UploadFile(ctx context.Context, fileName string, fil return r.GetPublicURL(fileName), err } -func (r *OssRepositoryImpl) GetPublicURL(fileName string) string { +func (r *S3FileClientImpl) GetPublicURL(fileName string) string { if fileName == "" { return "" } diff --git a/internal/common/.DS_Store b/internal/common/.DS_Store deleted file mode 100644 index ce4c8f3336581445b7ced3f2494ee35e35b97217..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKOHRWu5PegkHi%7^EPVxT5LM*_TmaHUuqYHrD#0#C=Z$BI5|K43XeP3sJ-^sb zik$%|j(im$B27-ZLAQ<>V4DilYDX$Ep4+esPVBnhpJs%RAV&ym(=A(mME&+%I)mb>! zT0&!zW92v)(n1jnC0eL>i6ItFeR99baWJ%Sh!-E?Z$1?-lHbn!DZ4`|!{~#7U|`C? zg-zFb|KIYHnJx0CAu$UEf`Na=fE3%i?S`B3yYCrLVvZtg4)V3(z)|K#H2GX<6s!yqPh`fvi!4W+eOB^RxY= z*ckv}eQlnA6@UesV35Ithprb-R%I8BWn7y33_p)0+YRNIbv?u)6!0msti zfpNWTKyz$0PhUFBk!|0TGvEw31J1x47~q>NvRE2=?+iEt&cHVVaz8{i!IW_@)T@J? zmH@SOjx83#iPN65uT$SZ#pFF{vl{;2K< z$ 0 +} diff --git a/internal/contract/response_error.go b/internal/contract/response_error.go new file mode 100644 index 0000000..542a749 --- /dev/null +++ b/internal/contract/response_error.go @@ -0,0 +1,33 @@ +package contract + +import "fmt" + +type ResponseError struct { + Code string `json:"code"` + Entity string `json:"entity"` + Cause string `json:"cause"` +} + +func NewResponseError(code, entity, cause string) *ResponseError { + return &ResponseError{ + Code: code, + Cause: cause, + Entity: entity, + } +} + +func (e *ResponseError) GetCode() string { + return e.Code +} + +func (e *ResponseError) GetEntity() string { + return e.Entity +} + +func (e *ResponseError) GetCause() string { + return e.Cause +} + +func (e *ResponseError) Error() string { + return fmt.Sprintf("%s: %s: %s", e.GetCode(), e.GetEntity(), e.GetCause()) +} diff --git a/internal/contract/user_contract.go b/internal/contract/user_contract.go new file mode 100644 index 0000000..c341c34 --- /dev/null +++ b/internal/contract/user_contract.go @@ -0,0 +1,69 @@ +package contract + +import ( + "time" + + "github.com/google/uuid" +) + +type CreateUserRequest struct { + OrganizationID uuid.UUID `json:"organization_id" validate:"required"` + OutletID *uuid.UUID `json:"outlet_id,omitempty"` + Name string `json:"name" validate:"required,min=1,max=255"` + Email string `json:"email" validate:"required,email"` + Password string `json:"password" validate:"required,min=6"` + Role string `json:"role" validate:"required,oneof=admin manager cashier waiter"` + Permissions map[string]interface{} `json:"permissions,omitempty"` +} + +type UpdateUserRequest struct { + Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"` + Email *string `json:"email,omitempty" validate:"omitempty,email"` + Role *string `json:"role,omitempty" validate:"omitempty,oneof=admin manager cashier waiter"` + OutletID *uuid.UUID `json:"outlet_id,omitempty"` + IsActive *bool `json:"is_active,omitempty"` + Permissions *map[string]interface{} `json:"permissions,omitempty"` +} + +type ChangePasswordRequest struct { + CurrentPassword string `json:"current_password" validate:"required"` + NewPassword string `json:"new_password" validate:"required,min=6"` +} + +type LoginRequest struct { + Email string `json:"email" validate:"required,email"` + Password string `json:"password" validate:"required"` +} + +type LoginResponse struct { + Token string `json:"token"` + ExpiresAt time.Time `json:"expires_at"` + User UserResponse `json:"user"` +} + +type UserResponse struct { + ID uuid.UUID `json:"id"` + OrganizationID uuid.UUID `json:"organization_id"` + OutletID *uuid.UUID `json:"outlet_id"` + Name string `json:"name"` + Email string `json:"email"` + Role string `json:"role"` + Permissions map[string]interface{} `json:"permissions"` + IsActive bool `json:"is_active"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type ListUsersRequest struct { + Page int `json:"page" validate:"min=1"` + Limit int `json:"limit" validate:"min=1,max=100"` + Role *string `json:"role,omitempty"` + OutletID *uuid.UUID `json:"outlet_id,omitempty"` + IsActive *bool `json:"is_active,omitempty"` + OrganizationID *uuid.UUID `json:"organization_id,omitempty"` +} + +type ListUsersResponse struct { + Users []UserResponse `json:"users"` + Pagination PaginationResponse `json:"pagination"` +} diff --git a/internal/common/db/database.go b/internal/db/database.go similarity index 68% rename from internal/common/db/database.go rename to internal/db/database.go index 0f667c3..daf3a2a 100644 --- a/internal/common/db/database.go +++ b/internal/db/database.go @@ -1,15 +1,13 @@ package db import ( + "apskel-pos-be/config" "fmt" - _ "github.com/lib/pq" "go.uber.org/zap" _ "gopkg.in/yaml.v3" "gorm.io/driver/postgres" "gorm.io/gorm" - - "enaklo-pos-be/config" ) func NewPostgres(c config.Database) (*gorm.DB, error) { @@ -19,21 +17,14 @@ func NewPostgres(c config.Database) (*gorm.DB, error) { db, err := gorm.Open(dialector, &gorm.Config{}) - //db, err := gorm.Open(dialector, &gorm.Config{ - // Logger: logger.Default.LogMode(logger.Info), // Enable GORM logging - //}) - if err != nil { return nil, err } zapCfg := zap.NewProductionConfig() - zapCfg.Level = zap.NewAtomicLevelAt(zap.ErrorLevel) // whatever minimum level + zapCfg.Level = zap.NewAtomicLevelAt(zap.ErrorLevel) zapCfg.DisableCaller = false - // logger, _ := zapCfg.Build() - // db = gorm.Open(sqldblogger.New(logger), db) - // ping the database to test the connection sqlDB, err := db.DB() if err != nil { return nil, err diff --git a/internal/entities/analytics.go b/internal/entities/analytics.go new file mode 100644 index 0000000..30f3b9b --- /dev/null +++ b/internal/entities/analytics.go @@ -0,0 +1,102 @@ +package entities + +import ( + "time" + + "github.com/google/uuid" +) + +// PaymentMethodAnalytics represents payment method analytics data +type PaymentMethodAnalytics struct { + PaymentMethodID uuid.UUID `json:"payment_method_id"` + PaymentMethodName string `json:"payment_method_name"` + PaymentMethodType string `json:"payment_method_type"` + TotalAmount float64 `json:"total_amount"` + OrderCount int64 `json:"order_count"` + PaymentCount int64 `json:"payment_count"` +} + +// SalesAnalytics represents sales analytics data +type SalesAnalytics struct { + Date time.Time `json:"date"` + Sales float64 `json:"sales"` + Orders int64 `json:"orders"` + Items int64 `json:"items"` + Tax float64 `json:"tax"` + Discount float64 `json:"discount"` + NetSales float64 `json:"net_sales"` +} + +// ProductAnalytics represents product analytics data +type ProductAnalytics struct { + ProductID uuid.UUID `json:"product_id"` + ProductName string `json:"product_name"` + CategoryID uuid.UUID `json:"category_id"` + CategoryName string `json:"category_name"` + QuantitySold int64 `json:"quantity_sold"` + Revenue float64 `json:"revenue"` + AveragePrice float64 `json:"average_price"` + OrderCount int64 `json:"order_count"` +} + +// DashboardOverview represents dashboard overview data +type DashboardOverview struct { + TotalSales float64 `json:"total_sales"` + TotalOrders int64 `json:"total_orders"` + AverageOrderValue float64 `json:"average_order_value"` + TotalCustomers int64 `json:"total_customers"` + VoidedOrders int64 `json:"voided_orders"` + RefundedOrders int64 `json:"refunded_orders"` +} + +// ProfitLossAnalytics represents profit and loss analytics data +type ProfitLossAnalytics struct { + Summary ProfitLossSummary `json:"summary"` + Data []ProfitLossData `json:"data"` + ProductData []ProductProfitData `json:"product_data"` +} + +// ProfitLossSummary represents profit and loss summary data +type ProfitLossSummary struct { + TotalRevenue float64 `json:"total_revenue"` + TotalCost float64 `json:"total_cost"` + GrossProfit float64 `json:"gross_profit"` + GrossProfitMargin float64 `json:"gross_profit_margin"` + TotalTax float64 `json:"total_tax"` + TotalDiscount float64 `json:"total_discount"` + NetProfit float64 `json:"net_profit"` + NetProfitMargin float64 `json:"net_profit_margin"` + TotalOrders int64 `json:"total_orders"` + AverageProfit float64 `json:"average_profit"` + ProfitabilityRatio float64 `json:"profitability_ratio"` +} + +// ProfitLossData represents profit and loss data by time period +type ProfitLossData struct { + Date time.Time `json:"date"` + Revenue float64 `json:"revenue"` + Cost float64 `json:"cost"` + GrossProfit float64 `json:"gross_profit"` + GrossProfitMargin float64 `json:"gross_profit_margin"` + Tax float64 `json:"tax"` + Discount float64 `json:"discount"` + NetProfit float64 `json:"net_profit"` + NetProfitMargin float64 `json:"net_profit_margin"` + Orders int64 `json:"orders"` +} + +// ProductProfitData represents profit data for individual products +type ProductProfitData struct { + ProductID uuid.UUID `json:"product_id"` + ProductName string `json:"product_name"` + CategoryID uuid.UUID `json:"category_id"` + CategoryName string `json:"category_name"` + QuantitySold int64 `json:"quantity_sold"` + Revenue float64 `json:"revenue"` + Cost float64 `json:"cost"` + GrossProfit float64 `json:"gross_profit"` + GrossProfitMargin float64 `json:"gross_profit_margin"` + AveragePrice float64 `json:"average_price"` + AverageCost float64 `json:"average_cost"` + ProfitPerUnit float64 `json:"profit_per_unit"` +} diff --git a/internal/entities/category.go b/internal/entities/category.go new file mode 100644 index 0000000..465d740 --- /dev/null +++ b/internal/entities/category.go @@ -0,0 +1,56 @@ +package entities + +import ( + "database/sql/driver" + "encoding/json" + "errors" + "time" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type Metadata map[string]interface{} + +func (m Metadata) Value() (driver.Value, error) { + return json.Marshal(m) +} + +func (m *Metadata) Scan(value interface{}) error { + if value == nil { + *m = make(Metadata) + return nil + } + + bytes, ok := value.([]byte) + if !ok { + return errors.New("type assertion to []byte failed") + } + + return json.Unmarshal(bytes, m) +} + +type Category struct { + ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"` + OrganizationID uuid.UUID `gorm:"type:uuid;not null;index" json:"organization_id" validate:"required"` + Name string `gorm:"not null;size:255" json:"name" validate:"required,min=1,max=255"` + Description *string `gorm:"type:text" json:"description"` + BusinessType string `gorm:"size:50;default:'restaurant'" json:"business_type"` + Metadata Metadata `gorm:"type:jsonb;default:'{}'" json:"metadata"` + CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` + + Organization Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"` + Products []Product `gorm:"foreignKey:CategoryID" json:"products,omitempty"` +} + +func (c *Category) BeforeCreate(tx *gorm.DB) error { + if c.ID == uuid.Nil { + c.ID = uuid.New() + } + return nil +} + +func (Category) TableName() string { + return "categories" +} diff --git a/internal/entities/customer.go b/internal/entities/customer.go new file mode 100644 index 0000000..e19451c --- /dev/null +++ b/internal/entities/customer.go @@ -0,0 +1,36 @@ +package entities + +import ( + "time" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type Customer struct { + ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"` + OrganizationID uuid.UUID `gorm:"type:uuid;not null;index" json:"organization_id" validate:"required"` + Name string `gorm:"not null;size:255" json:"name" validate:"required"` + Email *string `gorm:"size:255;uniqueIndex" json:"email,omitempty"` + Phone *string `gorm:"size:20" json:"phone,omitempty"` + Address *string `gorm:"size:500" json:"address,omitempty"` + IsDefault bool `gorm:"default:false" json:"is_default"` + IsActive bool `gorm:"default:true" json:"is_active"` + Metadata Metadata `gorm:"type:jsonb;default:'{}'" json:"metadata"` + CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` + + Organization Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"` + Orders []Order `gorm:"foreignKey:CustomerID" json:"orders,omitempty"` +} + +func (c *Customer) BeforeCreate(tx *gorm.DB) error { + if c.ID == uuid.Nil { + c.ID = uuid.New() + } + return nil +} + +func (Customer) TableName() string { + return "customers" +} diff --git a/internal/entities/entities.go b/internal/entities/entities.go new file mode 100644 index 0000000..c0aec65 --- /dev/null +++ b/internal/entities/entities.go @@ -0,0 +1,26 @@ +package entities + +import "gorm.io/gorm" + +func GetAllEntities() []interface{} { + return []interface{}{ + &Organization{}, + &Outlet{}, + &OutletSetting{}, + &User{}, + &Category{}, + &Product{}, + &ProductVariant{}, + &Inventory{}, + &Order{}, + &OrderItem{}, + &PaymentMethod{}, + &Payment{}, + &Customer{}, + // Analytics entities are not database tables, they are query results + } +} + +func AutoMigrate(db *gorm.DB) error { + return db.AutoMigrate(GetAllEntities()...) +} diff --git a/internal/entities/file.go b/internal/entities/file.go new file mode 100644 index 0000000..bbbdbb4 --- /dev/null +++ b/internal/entities/file.go @@ -0,0 +1,28 @@ +package entities + +import ( + "time" + + "github.com/google/uuid" +) + +type File struct { + ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"` + OrganizationID uuid.UUID `gorm:"type:uuid;not null" json:"organization_id"` + UserID uuid.UUID `gorm:"type:uuid;not null" json:"user_id"` + FileName string `gorm:"size:255;not null" json:"file_name"` + OriginalName string `gorm:"size:255;not null" json:"original_name"` + FileURL string `gorm:"size:500;not null" json:"file_url"` + FileSize int64 `gorm:"not null" json:"file_size"` + MimeType string `gorm:"size:100;not null" json:"mime_type"` + FileType string `gorm:"size:50;not null" json:"file_type"` // image, document, video, etc. + UploadPath string `gorm:"size:500;not null" json:"upload_path"` + IsPublic bool `gorm:"default:true" json:"is_public"` + Metadata Metadata `gorm:"type:jsonb" json:"metadata"` + CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` +} + +func (File) TableName() string { + return "files" +} diff --git a/internal/entities/inventory.go b/internal/entities/inventory.go new file mode 100644 index 0000000..3ef53a3 --- /dev/null +++ b/internal/entities/inventory.go @@ -0,0 +1,42 @@ +package entities + +import ( + "time" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type Inventory struct { + ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"` + OutletID uuid.UUID `gorm:"type:uuid;not null;index" json:"outlet_id" validate:"required"` + ProductID uuid.UUID `gorm:"type:uuid;not null;index" json:"product_id" validate:"required"` + Quantity int `gorm:"not null;default:0" json:"quantity" validate:"min=0"` + ReorderLevel int `gorm:"default:0" json:"reorder_level" validate:"min=0"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` + + Outlet Outlet `gorm:"foreignKey:OutletID" json:"outlet,omitempty"` + Product Product `gorm:"foreignKey:ProductID" json:"product,omitempty"` +} + +func (i *Inventory) BeforeCreate(tx *gorm.DB) error { + if i.ID == uuid.Nil { + i.ID = uuid.New() + } + return nil +} + +func (Inventory) TableName() string { + return "inventory" +} + +func (i *Inventory) IsLowStock() bool { + return i.Quantity <= i.ReorderLevel +} + +func (i *Inventory) UpdateQuantity(delta int) { + i.Quantity += delta + if i.Quantity < 0 { + i.Quantity = 0 + } +} diff --git a/internal/entities/order.go b/internal/entities/order.go new file mode 100644 index 0000000..b8575d7 --- /dev/null +++ b/internal/entities/order.go @@ -0,0 +1,110 @@ +package entities + +import ( + "fmt" + "time" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type OrderType string +type OrderStatus string +type PaymentStatus string + +const ( + OrderTypeDineIn OrderType = "dine_in" + OrderTypeTakeout OrderType = "takeout" + OrderTypeDelivery OrderType = "delivery" +) + +const ( + OrderStatusPending OrderStatus = "pending" + OrderStatusPreparing OrderStatus = "preparing" + OrderStatusReady OrderStatus = "ready" + OrderStatusCompleted OrderStatus = "completed" + OrderStatusCancelled OrderStatus = "cancelled" +) + +const ( + PaymentStatusPending PaymentStatus = "pending" + PaymentStatusCompleted PaymentStatus = "completed" + PaymentStatusFailed PaymentStatus = "failed" + PaymentStatusRefunded PaymentStatus = "refunded" + PaymentStatusPartiallyRefunded PaymentStatus = "partial-refunded" +) + +type Order struct { + ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"` + OrganizationID uuid.UUID `gorm:"type:uuid;not null;index" json:"organization_id" validate:"required"` + OutletID uuid.UUID `gorm:"type:uuid;not null;index" json:"outlet_id" validate:"required"` + UserID uuid.UUID `gorm:"type:uuid;not null;index" json:"user_id" validate:"required"` + CustomerID *uuid.UUID `gorm:"type:uuid;index" json:"customer_id"` + OrderNumber string `gorm:"uniqueIndex;not null;size:50" json:"order_number" validate:"required"` + TableNumber *string `gorm:"size:20" json:"table_number"` + OrderType OrderType `gorm:"not null;size:50" json:"order_type" validate:"required,oneof=dine_in takeout delivery"` + Status OrderStatus `gorm:"default:'pending';size:50" json:"status"` + Subtotal float64 `gorm:"type:decimal(10,2);not null" json:"subtotal" validate:"required,min=0"` + TaxAmount float64 `gorm:"type:decimal(10,2);not null" json:"tax_amount" validate:"required,min=0"` + DiscountAmount float64 `gorm:"type:decimal(10,2);default:0.00" json:"discount_amount" validate:"min=0"` + TotalAmount float64 `gorm:"type:decimal(10,2);not null" json:"total_amount" validate:"required,min=0"` + TotalCost float64 `gorm:"type:decimal(10,2);default:0.00" json:"total_cost"` + PaymentStatus PaymentStatus `gorm:"default:'pending';size:50" json:"payment_status"` + RefundAmount float64 `gorm:"type:decimal(10,2);default:0.00" json:"refund_amount"` + IsVoid bool `gorm:"default:false" json:"is_void"` + IsRefund bool `gorm:"default:false" json:"is_refund"` + VoidReason *string `gorm:"size:255" json:"void_reason,omitempty"` + VoidedAt *time.Time `gorm:"" json:"voided_at,omitempty"` + VoidedBy *uuid.UUID `gorm:"type:uuid" json:"voided_by,omitempty"` + RefundReason *string `gorm:"size:255" json:"refund_reason,omitempty"` + RefundedAt *time.Time `gorm:"" json:"refunded_at,omitempty"` + RefundedBy *uuid.UUID `gorm:"type:uuid" json:"refunded_by,omitempty"` + Metadata Metadata `gorm:"type:jsonb;default:'{}'" json:"metadata"` + CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` + + Organization Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"` + Outlet Outlet `gorm:"foreignKey:OutletID" json:"outlet,omitempty"` + User User `gorm:"foreignKey:UserID" json:"user,omitempty"` + OrderItems []OrderItem `gorm:"foreignKey:OrderID" json:"order_items,omitempty"` + Payments []Payment `gorm:"foreignKey:OrderID" json:"payments,omitempty"` +} + +func (o *Order) BeforeCreate(tx *gorm.DB) error { + if o.ID == uuid.Nil { + o.ID = uuid.New() + } + + if o.OrderNumber == "" { + timestamp := time.Now().Unix() + o.OrderNumber = fmt.Sprintf("ORD/%d", timestamp) + } + + return nil +} + +func (Order) TableName() string { + return "orders" +} + +func (o *Order) CanBeModified() bool { + return o.Status == OrderStatusPending +} + +func (o *Order) CanBeCancelled() bool { + return o.Status != OrderStatusCompleted && o.Status != OrderStatusCancelled +} + +func (o *Order) GetTotalPaid() float64 { + var total float64 + for _, payment := range o.Payments { + if payment.Status == PaymentTransactionStatusCompleted { + total += payment.Amount + } + } + return total +} + +func (o *Order) IsFullyPaid() bool { + return o.GetTotalPaid() >= o.TotalAmount +} diff --git a/internal/entities/order_item.go b/internal/entities/order_item.go new file mode 100644 index 0000000..14a708c --- /dev/null +++ b/internal/entities/order_item.go @@ -0,0 +1,89 @@ +package entities + +import ( + "database/sql/driver" + "encoding/json" + "errors" + "time" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type Modifiers []map[string]interface{} + +func (m Modifiers) Value() (driver.Value, error) { + return json.Marshal(m) +} + +func (m *Modifiers) Scan(value interface{}) error { + if value == nil { + *m = make(Modifiers, 0) + return nil + } + + bytes, ok := value.([]byte) + if !ok { + return errors.New("type assertion to []byte failed") + } + + return json.Unmarshal(bytes, m) +} + +type OrderItemStatus string + +const ( + OrderItemStatusPending OrderItemStatus = "pending" + OrderItemStatusPreparing OrderItemStatus = "preparing" + OrderItemStatusReady OrderItemStatus = "ready" + OrderItemStatusServed OrderItemStatus = "served" + OrderItemStatusCancelled OrderItemStatus = "cancelled" +) + +type OrderItem struct { + ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"` + OrderID uuid.UUID `gorm:"type:uuid;not null;index" json:"order_id" validate:"required"` + ProductID uuid.UUID `gorm:"type:uuid;not null;index" json:"product_id" validate:"required"` + ProductVariantID *uuid.UUID `gorm:"type:uuid;index" json:"product_variant_id"` + Quantity int `gorm:"not null" json:"quantity" validate:"required,min=1"` + UnitPrice float64 `gorm:"type:decimal(10,2);not null" json:"unit_price" validate:"required,min=0"` + TotalPrice float64 `gorm:"type:decimal(10,2);not null" json:"total_price" validate:"required,min=0"` + UnitCost float64 `gorm:"type:decimal(10,2);default:0.00" json:"unit_cost"` + TotalCost float64 `gorm:"type:decimal(10,2);default:0.00" json:"total_cost"` + RefundAmount float64 `gorm:"type:decimal(10,2);default:0.00" json:"refund_amount"` + RefundQuantity int `gorm:"default:0" json:"refund_quantity"` + IsPartiallyRefunded bool `gorm:"default:false" json:"is_partially_refunded"` + IsFullyRefunded bool `gorm:"default:false" json:"is_fully_refunded"` + RefundReason *string `gorm:"size:255" json:"refund_reason,omitempty"` + RefundedAt *time.Time `gorm:"" json:"refunded_at,omitempty"` + RefundedBy *uuid.UUID `gorm:"type:uuid" json:"refunded_by,omitempty"` + Modifiers Modifiers `gorm:"type:jsonb;default:'[]'" json:"modifiers"` + Notes *string `gorm:"size:500" json:"notes,omitempty"` + Metadata Metadata `gorm:"type:jsonb;default:'{}'" json:"metadata"` + Status OrderItemStatus `gorm:"default:'pending';size:50" json:"status"` + CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` + + Order Order `gorm:"foreignKey:OrderID" json:"order,omitempty"` + Product Product `gorm:"foreignKey:ProductID" json:"product,omitempty"` + ProductVariant *ProductVariant `gorm:"foreignKey:ProductVariantID" json:"product_variant,omitempty"` +} + +func (oi *OrderItem) BeforeCreate(tx *gorm.DB) error { + if oi.ID == uuid.Nil { + oi.ID = uuid.New() + } + return nil +} + +func (OrderItem) TableName() string { + return "order_items" +} + +func (oi *OrderItem) CalculateTotalPrice() { + oi.TotalPrice = float64(oi.Quantity) * oi.UnitPrice +} + +func (oi *OrderItem) CanBeModified() bool { + return oi.Status == OrderItemStatusPending +} diff --git a/internal/entities/order_sequence.go b/internal/entities/order_sequence.go new file mode 100644 index 0000000..9ab88ca --- /dev/null +++ b/internal/entities/order_sequence.go @@ -0,0 +1,33 @@ +package entities + +import ( + "time" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type OrderSequence struct { + ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"` + OrganizationID uuid.UUID `gorm:"type:uuid;not null;index" json:"organization_id"` + OutletID uuid.UUID `gorm:"type:uuid;not null;index" json:"outlet_id"` + Year int `gorm:"not null" json:"year"` + Month int `gorm:"not null" json:"month"` + SequenceNumber int `gorm:"not null;default:0" json:"sequence_number"` + CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` + + Organization Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"` + Outlet Outlet `gorm:"foreignKey:OutletID" json:"outlet,omitempty"` +} + +func (os *OrderSequence) BeforeCreate(tx *gorm.DB) error { + if os.ID == uuid.Nil { + os.ID = uuid.New() + } + return nil +} + +func (OrderSequence) TableName() string { + return "order_sequences" +} diff --git a/internal/entities/organization.go b/internal/entities/organization.go new file mode 100644 index 0000000..1cefd9f --- /dev/null +++ b/internal/entities/organization.go @@ -0,0 +1,34 @@ +package entities + +import ( + "time" + + "github.com/google/uuid" + + "gorm.io/gorm" +) + +type Organization struct { + ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"` + Name string `gorm:"not null;size:255" json:"name" validate:"required,min=1,max=255"` + Email *string `gorm:"size:255" json:"email" validate:"omitempty,email"` + PhoneNumber *string `gorm:"size:20" json:"phone_number" validate:"omitempty"` + PlanType string `gorm:"not null;size:50" json:"plan_type" validate:"required,oneof=basic premium enterprise"` + CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` + + Outlets []Outlet `gorm:"foreignKey:OrganizationID" json:"outlets,omitempty"` + Users []User `gorm:"foreignKey:OrganizationID" json:"users,omitempty"` +} + +func (o *Organization) BeforeCreate(tx *gorm.DB) error { + if o.ID == uuid.Nil { + id := uuid.New() + o.ID = id + } + return nil +} + +func (Organization) TableName() string { + return "organizations" +} diff --git a/internal/entities/outlet.go b/internal/entities/outlet.go new file mode 100644 index 0000000..e947851 --- /dev/null +++ b/internal/entities/outlet.go @@ -0,0 +1,38 @@ +package entities + +import ( + "time" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type Outlet struct { + ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"` + OrganizationID uuid.UUID `gorm:"type:uuid;not null;index" json:"organization_id" validate:"required"` + Name string `gorm:"not null;size:255" json:"name" validate:"required,min=1,max=255"` + Address *string `gorm:"type:text" json:"address"` + Timezone *string `gorm:"size:50" json:"timezone"` + Currency string `gorm:"size:3;default:'USD'" json:"currency" validate:"len=3"` + TaxRate float64 `gorm:"type:decimal(5,4);default:0.0000" json:"tax_rate" validate:"min=0,max=1"` + IsActive bool `gorm:"default:true" json:"is_active"` + CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` + + Organization Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"` + Users []User `gorm:"foreignKey:OutletID" json:"users,omitempty"` + Orders []Order `gorm:"foreignKey:OutletID" json:"orders,omitempty"` + Inventory []Inventory `gorm:"foreignKey:OutletID" json:"inventory,omitempty"` + Settings []OutletSetting `gorm:"foreignKey:OutletID" json:"settings,omitempty"` +} + +func (o *Outlet) BeforeCreate(tx *gorm.DB) error { + if o.ID == uuid.Nil { + o.ID = uuid.New() + } + return nil +} + +func (Outlet) TableName() string { + return "outlets" +} diff --git a/internal/entities/outlet_setting.go b/internal/entities/outlet_setting.go new file mode 100644 index 0000000..d191f85 --- /dev/null +++ b/internal/entities/outlet_setting.go @@ -0,0 +1,30 @@ +package entities + +import ( + "time" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type OutletSetting struct { + ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"` + OutletID uuid.UUID `gorm:"type:uuid;not null;index" json:"outlet_id" validate:"required"` + Key string `gorm:"not null;size:255;index" json:"key" validate:"required,min=1,max=255"` + Value string `gorm:"type:text" json:"value"` + CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` + + Outlet Outlet `gorm:"foreignKey:OutletID" json:"outlet,omitempty"` +} + +func (os *OutletSetting) BeforeCreate(tx *gorm.DB) error { + if os.ID == uuid.Nil { + os.ID = uuid.New() + } + return nil +} + +func (OutletSetting) TableName() string { + return "outlet_settings" +} diff --git a/internal/entities/payment.go b/internal/entities/payment.go new file mode 100644 index 0000000..b03f914 --- /dev/null +++ b/internal/entities/payment.go @@ -0,0 +1,93 @@ +package entities + +import ( + "time" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type PaymentMethodType string + +const ( + PaymentMethodTypeCash PaymentMethodType = "cash" + PaymentMethodTypeCard PaymentMethodType = "card" + PaymentMethodTypeDigitalWallet PaymentMethodType = "digital_wallet" +) + +type PaymentMethod struct { + ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"` + OrganizationID uuid.UUID `gorm:"type:uuid;not null;index" json:"organization_id" validate:"required"` + Name string `gorm:"not null;size:100" json:"name" validate:"required,min=1,max=100"` + Type PaymentMethodType `gorm:"not null;size:50" json:"type" validate:"required,oneof=cash card digital_wallet"` + Processor *string `gorm:"size:100" json:"processor"` + Configuration Metadata `gorm:"type:jsonb;default:'{}'" json:"configuration"` + IsActive bool `gorm:"default:true" json:"is_active"` + CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` + + Organization Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"` + Payments []Payment `gorm:"foreignKey:PaymentMethodID" json:"payments,omitempty"` +} + +func (pm *PaymentMethod) BeforeCreate(tx *gorm.DB) error { + if pm.ID == uuid.Nil { + pm.ID = uuid.New() + } + return nil +} + +func (PaymentMethod) TableName() string { + return "payment_methods" +} + +type PaymentTransactionStatus string + +const ( + PaymentTransactionStatusPending PaymentTransactionStatus = "pending" + PaymentTransactionStatusCompleted PaymentTransactionStatus = "completed" + PaymentTransactionStatusFailed PaymentTransactionStatus = "failed" + PaymentTransactionStatusRefunded PaymentTransactionStatus = "refunded" +) + +type Payment struct { + ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"` + OrderID uuid.UUID `gorm:"type:uuid;not null;index" json:"order_id" validate:"required"` + PaymentMethodID uuid.UUID `gorm:"type:uuid;not null;index" json:"payment_method_id" validate:"required"` + Amount float64 `gorm:"type:decimal(10,2);not null" json:"amount" validate:"required,min=0"` + Status PaymentTransactionStatus `gorm:"default:'pending';size:50" json:"status"` + TransactionID *string `gorm:"size:255" json:"transaction_id"` + SplitNumber int `gorm:"default:1" json:"split_number"` + SplitTotal int `gorm:"default:1" json:"split_total"` + SplitDescription *string `gorm:"size:255" json:"split_description,omitempty"` + RefundAmount float64 `gorm:"type:decimal(10,2);default:0.00" json:"refund_amount"` + RefundReason *string `gorm:"size:255" json:"refund_reason,omitempty"` + RefundedAt *time.Time `gorm:"" json:"refunded_at,omitempty"` + RefundedBy *uuid.UUID `gorm:"type:uuid" json:"refunded_by,omitempty"` + Metadata Metadata `gorm:"type:jsonb;default:'{}'" json:"metadata"` + CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` + + Order Order `gorm:"foreignKey:OrderID" json:"order,omitempty"` + PaymentMethod PaymentMethod `gorm:"foreignKey:PaymentMethodID" json:"payment_method,omitempty"` + PaymentOrderItems []PaymentOrderItem `gorm:"foreignKey:PaymentID" json:"payment_order_items,omitempty"` +} + +func (p *Payment) BeforeCreate(tx *gorm.DB) error { + if p.ID == uuid.Nil { + p.ID = uuid.New() + } + return nil +} + +func (Payment) TableName() string { + return "payments" +} + +func (p *Payment) CanBeRefunded() bool { + return p.Status == PaymentTransactionStatusCompleted +} + +func (p *Payment) IsSuccessful() bool { + return p.Status == PaymentTransactionStatusCompleted +} diff --git a/internal/entities/payment_order_item.go b/internal/entities/payment_order_item.go new file mode 100644 index 0000000..ee78b98 --- /dev/null +++ b/internal/entities/payment_order_item.go @@ -0,0 +1,31 @@ +package entities + +import ( + "time" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type PaymentOrderItem struct { + ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"` + PaymentID uuid.UUID `gorm:"type:uuid;not null;index" json:"payment_id"` + OrderItemID uuid.UUID `gorm:"type:uuid;not null;index" json:"order_item_id"` + Amount float64 `gorm:"type:decimal(10,2);not null" json:"amount"` + CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` + + Payment Payment `gorm:"foreignKey:PaymentID" json:"payment,omitempty"` + OrderItem OrderItem `gorm:"foreignKey:OrderItemID" json:"order_item,omitempty"` +} + +func (poi *PaymentOrderItem) BeforeCreate(tx *gorm.DB) error { + if poi.ID == uuid.Nil { + poi.ID = uuid.New() + } + return nil +} + +func (PaymentOrderItem) TableName() string { + return "payment_order_items" +} diff --git a/internal/entities/product.go b/internal/entities/product.go new file mode 100644 index 0000000..c590375 --- /dev/null +++ b/internal/entities/product.go @@ -0,0 +1,66 @@ +package entities + +import ( + "time" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type Product struct { + ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"` + OrganizationID uuid.UUID `gorm:"type:uuid;not null;index" json:"organization_id" validate:"required"` + CategoryID uuid.UUID `gorm:"type:uuid;not null;index" json:"category_id" validate:"required"` + SKU *string `gorm:"size:100;index" json:"sku"` + Name string `gorm:"not null;size:255" json:"name" validate:"required,min=1,max=255"` + Description *string `gorm:"type:text" json:"description"` + Price float64 `gorm:"type:decimal(10,2);not null" json:"price" validate:"required,min=0"` + Cost float64 `gorm:"type:decimal(10,2);default:0.00" json:"cost" validate:"min=0"` + BusinessType string `gorm:"size:50;default:'restaurant'" json:"business_type"` + Metadata Metadata `gorm:"type:jsonb;default:'{}'" json:"metadata"` + IsActive bool `gorm:"default:true" json:"is_active"` + CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` + + Organization Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"` + Category Category `gorm:"foreignKey:CategoryID" json:"category,omitempty"` + ProductVariants []ProductVariant `gorm:"foreignKey:ProductID" json:"variants,omitempty"` + Inventory []Inventory `gorm:"foreignKey:ProductID" json:"inventory,omitempty"` + OrderItems []OrderItem `gorm:"foreignKey:ProductID" json:"order_items,omitempty"` +} + +func (p *Product) BeforeCreate(tx *gorm.DB) error { + if p.ID == uuid.Nil { + p.ID = uuid.New() + } + return nil +} + +func (Product) TableName() string { + return "products" +} + +type ProductVariant struct { + ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"` + ProductID uuid.UUID `gorm:"type:uuid;not null;index" json:"product_id" validate:"required"` + Name string `gorm:"not null;size:255" json:"name" validate:"required,min=1,max=255"` + PriceModifier float64 `gorm:"type:decimal(10,2);default:0.00" json:"price_modifier"` + Cost float64 `gorm:"type:decimal(10,2);default:0.00" json:"cost" validate:"min=0"` + Metadata Metadata `gorm:"type:jsonb;default:'{}'" json:"metadata"` + CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` + + Product Product `gorm:"foreignKey:ProductID" json:"product,omitempty"` + OrderItems []OrderItem `gorm:"foreignKey:ProductVariantID" json:"order_items,omitempty"` +} + +func (pv *ProductVariant) BeforeCreate(tx *gorm.DB) error { + if pv.ID == uuid.Nil { + pv.ID = uuid.New() + } + return nil +} + +func (ProductVariant) TableName() string { + return "product_variants" +} diff --git a/internal/entities/user.go b/internal/entities/user.go new file mode 100644 index 0000000..d69214b --- /dev/null +++ b/internal/entities/user.go @@ -0,0 +1,94 @@ +package entities + +import ( + "database/sql/driver" + "encoding/json" + "errors" + "time" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type UserRole string + +const ( + RoleAdmin UserRole = "admin" + RoleManager UserRole = "manager" + RoleCashier UserRole = "cashier" + RoleWaiter UserRole = "waiter" +) + +type Permissions map[string]interface{} + +func (p Permissions) Value() (driver.Value, error) { + return json.Marshal(p) +} + +func (p *Permissions) Scan(value interface{}) error { + if value == nil { + *p = make(Permissions) + return nil + } + + bytes, ok := value.([]byte) + if !ok { + return errors.New("type assertion to []byte failed") + } + + return json.Unmarshal(bytes, p) +} + +type User struct { + ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"` + OrganizationID uuid.UUID `gorm:"type:uuid;not null;index" json:"organization_id" validate:"required"` + OutletID *uuid.UUID `gorm:"type:uuid;index" json:"outlet_id"` + Name string `gorm:"not null;size:255" json:"name" validate:"required,min=1,max=255"` + Email string `gorm:"uniqueIndex;not null;size:255" json:"email" validate:"required,email"` + PasswordHash string `gorm:"not null;size:255" json:"-"` + Role UserRole `gorm:"not null;size:50" json:"role" validate:"required,oneof=admin manager cashier waiter"` + Permissions Permissions `gorm:"type:jsonb;default:'{}'" json:"permissions"` + IsActive bool `gorm:"default:true" json:"is_active"` + CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` + Organization Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"` + Outlet *Outlet `gorm:"foreignKey:OutletID" json:"outlet,omitempty"` + Orders []Order `gorm:"foreignKey:UserID" json:"orders,omitempty"` +} + +func (u *User) BeforeCreate(tx *gorm.DB) error { + if u.ID == uuid.Nil { + u.ID = uuid.New() + } + return nil +} + +func (User) TableName() string { + return "users" +} + +func (u *User) HasPermission(permission string) bool { + if u.Role == RoleAdmin { + return true + } + + if value, exists := u.Permissions[permission]; exists { + if hasPermission, ok := value.(bool); ok { + return hasPermission + } + } + + return false +} + +func (u *User) CanAccessOutlet(outletID uuid.UUID) bool { + if u.Role == RoleAdmin { + return true + } + + if u.OutletID != nil && *u.OutletID == outletID { + return true + } + + return false +} diff --git a/internal/entity/auth.go b/internal/entity/auth.go deleted file mode 100644 index 42bb73d..0000000 --- a/internal/entity/auth.go +++ /dev/null @@ -1,219 +0,0 @@ -package entity - -import ( - "enaklo-pos-be/internal/constants/role" - "enaklo-pos-be/internal/constants/userstatus" - "time" -) - -type AuthData struct { - Token string `json:"token"` - UserID int64 `gorm:"column:user_id"` - RoleID int `gorm:"column:role_id"` - OrganizationID int64 `gorm:"column:organization_id"` -} - -type UserDB struct { - ID int64 `gorm:"primary_key;column:id" json:"id"` - Name string `gorm:"column:name" json:"name"` - Email string `gorm:"column:email" json:"email"` - Password string `gorm:"column:password" json:"-"` - Status userstatus.UserStatus `gorm:"column:status" json:"status"` - UserType string `gorm:"column:user_type" json:"user_type"` - PhoneNumber string `gorm:"column:phone_number" json:"phone_number"` - NIK string `gorm:"column:nik" json:"nik"` - RoleID int64 `gorm:"column:role_id" json:"role_id"` - RoleName string `gorm:"column:role_name" json:"role_name"` - PartnerID *int64 `gorm:"column:partner_id" json:"partner_id"` - SiteID *int64 `gorm:"column:site_id" json:"site_id"` - SiteName string `gorm:"column:name" json:"site_name"` - PartnerName string `gorm:"column:partner_name" json:"partner_name"` - PartnerStatus string `gorm:"column:partner_status" json:"partner_status"` - CreatedAt time.Time `gorm:"column:created_at" json:"created_at"` - UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"` - DeletedAt *time.Time `gorm:"column:deleted_at" json:"deleted_at"` - CreatedBy int64 `gorm:"column:created_by" json:"created_by"` - UpdatedBy int64 `gorm:"column:updated_by" json:"updated_by"` - ResetPassword bool `gorm:"column:reset_password" json:"reset_password"` -} - -func (u *UserDB) ToCustomer() *Customer { - if u == nil { - return &Customer{} - } - - userEntity := &Customer{ - ID: u.ID, - Name: u.Name, - Email: u.Email, - PhoneNumber: u.PhoneNumber, - Status: u.Status, - CreatedAt: u.CreatedAt, - UpdatedAt: u.UpdatedAt, - RoleID: role.Role(u.RoleID), - RoleName: u.RoleName, - PartnerID: u.PartnerID, - PartnerName: u.PartnerName, - SiteID: u.SiteID, - SiteName: u.SiteName, - ResetPassword: u.ResetPassword, - } - - return userEntity -} - -func (u *UserDB) ToUser() *User { - if u == nil { - return &User{} - } - - userEntity := &User{ - ID: u.ID, - Name: u.Name, - Email: u.Email, - NIK: u.NIK, - PhoneNumber: u.PhoneNumber, - Status: u.Status, - CreatedAt: u.CreatedAt, - UpdatedAt: u.UpdatedAt, - RoleID: role.Role(u.RoleID), - RoleName: u.RoleName, - PartnerID: u.PartnerID, - PartnerName: u.PartnerName, - SiteID: u.SiteID, - SiteName: u.SiteName, - ResetPassword: u.ResetPassword, - } - - return userEntity -} - -func (u *UserDB) ToUserRoleDB() *UserRoleDB { - if u == nil { - return &UserRoleDB{} - } - - userRole := &UserRoleDB{ - ID: 0, - UserID: u.ID, - RoleID: u.RoleID, - PartnerID: u.PartnerID, - CreatedAt: u.CreatedAt, - UpdatedAt: u.UpdatedAt, - SiteID: u.SiteID, - } - - return userRole -} - -func (UserDB) TableName() string { - return "users" -} - -func (u *UserDB) ToUserAuthenticate(signedToken string, license PartnerLicense) *AuthenticateUser { - return &AuthenticateUser{ - ID: u.ID, - Token: signedToken, - Name: u.Name, - RoleID: role.Role(u.RoleID), - RoleName: u.RoleName, - PartnerID: u.PartnerID, - PartnerName: u.PartnerName, - PartnerStatus: u.PartnerStatus, - SiteID: u.SiteID, - SiteName: u.SiteName, - ResetPassword: u.ResetPassword, - PartnerLicense: license, - UserType: u.UserType, - } -} - -type UserSearch struct { - Search string - Name string - RoleID int64 - PartnerID int64 - SiteID int64 - Limit int - Offset int -} - -type UserList []*UserDB - -func (b *UserList) ToUserList() []*User { - var users []*User - for _, user := range *b { - users = append(users, user.ToUser()) - } - return users -} - -func (u *UserDB) ToUpdatedUser(req User) error { - if req.Name != "" { - u.Name = req.Name - } - - if req.Email != "" { - u.Email = req.Email - } - - if req.PhoneNumber != "" { - u.PhoneNumber = req.PhoneNumber - } - - if req.NIK != "" { - u.NIK = req.NIK - } - - u.RoleID = int64(req.RoleID) - - if req.Password != "" { - hashedPassword, err := req.HashedPassword(req.Password) - - if err != nil { - return err - } - u.Password = hashedPassword - } - - u.SiteID = req.SiteID - u.PartnerID = req.PartnerID - - return nil -} - -func (o *UserDB) SetDeleted(updatedby int64) { - currentTime := time.Now() - o.DeletedAt = ¤tTime - o.UpdatedBy = updatedby - o.Status = userstatus.Inactive -} - -type MemberList []*Customer -type CustomerList []*UserDB - -type CustomerSearch struct { - Search string - Name string - RoleID int64 - PartnerID int64 - SiteID int64 - Limit int - Offset int -} - -func (b *CustomerList) ToCustomerList() []*Customer { - var users []*Customer - for _, user := range *b { - if len(user.Name) > 0 { - users = append(users, user.ToCustomer()) - } - } - return users -} - -type MemberSearch struct { - Search string - Limit int - Offset int -} diff --git a/internal/entity/balance.go b/internal/entity/balance.go deleted file mode 100644 index 94e6777..0000000 --- a/internal/entity/balance.go +++ /dev/null @@ -1,25 +0,0 @@ -package entity - -type Balance struct { - PartnerID int64 - Balance float64 - AuthBalance float64 -} - -type BalanceWithdrawInquiry struct { - PartnerID int64 - Amount int64 -} - -type BalanceWithdrawInquiryResponse struct { - PartnerID int64 - Amount int64 - Total int64 - Fee int64 - Token string -} - -type WalletWithdrawResponse struct { - TransactionID string - Status string -} diff --git a/internal/entity/casheer_session.go b/internal/entity/casheer_session.go deleted file mode 100644 index e1c4feb..0000000 --- a/internal/entity/casheer_session.go +++ /dev/null @@ -1,28 +0,0 @@ -package entity - -import "time" - -type CashierSession struct { - ID int64 - PartnerID int64 - CashierID int64 - OpenedAt time.Time - ClosedAt *time.Time - OpeningAmount float64 - ClosingAmount *float64 - ExpectedAmount *float64 - Status string -} - -type PaymentSummary struct { - PaymentType string - PaymentProvider string - TotalAmount float64 -} - -type CashierSessionReport struct { - SessionID int64 - ExpectedAmount float64 - ClosingAmount float64 - Payments []PaymentSummary -} diff --git a/internal/entity/category.go b/internal/entity/category.go deleted file mode 100644 index a73aae2..0000000 --- a/internal/entity/category.go +++ /dev/null @@ -1,9 +0,0 @@ -package entity - -type Category struct { - ID int64 - PartnerID int64 - Name string - CreatedAt int64 - UpdatedAt int64 -} diff --git a/internal/entity/cust.go b/internal/entity/cust.go deleted file mode 100644 index eaa9754..0000000 --- a/internal/entity/cust.go +++ /dev/null @@ -1,18 +0,0 @@ -package entity - -import "time" - -type CustomerResolutionRequest struct { - ID *int64 - Name string - Email string - PhoneNumber string - BirthDate time.Time - Password string -} - -type CustomerCheckResponse struct { - Exists bool - Customer *Customer - Message string -} diff --git a/internal/entity/customer.go b/internal/entity/customer.go deleted file mode 100644 index cab87f6..0000000 --- a/internal/entity/customer.go +++ /dev/null @@ -1,2 +0,0 @@ -package entity - diff --git a/internal/entity/discovery.go b/internal/entity/discovery.go deleted file mode 100644 index fbb5990..0000000 --- a/internal/entity/discovery.go +++ /dev/null @@ -1,43 +0,0 @@ -package entity - -type DiscoverySearch struct { - Lat float64 - Long float64 - Name string - Region string - Status string - Discover string - Offset int - Limit int - Radius int -} - -type DiscoverySearchResp struct { - ExploreRegions []ExploreRegion `json:"exploreRegions"` - ExploreDestinations []ExploreDestination `json:"exploreDestinations"` - MustVisit []MustVisit `json:"mustVisit"` -} - -type ExploreRegion struct { - Name string `json:"name"` -} - -type ExploreDestination struct { - Name string `json:"name"` - ImageURL string `json:"image_url"` -} - -type MustVisit struct { - SiteID int64 `json:"site_id"` - Name string `json:"name"` - Location string `json:"location"` - Rating float64 `json:"rating"` - ReviewCount int `json:"reviewCount"` - Price float64 `json:"price"` - ImageURL string `json:"imageUrl"` - Region string `json:"region"` - Regency string `json:"regency"` -} - -type DiscoveryGetByIDResp struct { -} diff --git a/internal/entity/email.go b/internal/entity/email.go deleted file mode 100644 index 0cf883d..0000000 --- a/internal/entity/email.go +++ /dev/null @@ -1,13 +0,0 @@ -package entity - -type ( - SendEmailNotificationParam struct { - Sender string - Recipient string - CcEmails []string - Subject string - TemplateName string - TemplatePath string - Data interface{} - } -) diff --git a/internal/entity/event.go b/internal/entity/event.go deleted file mode 100644 index 915c3bb..0000000 --- a/internal/entity/event.go +++ /dev/null @@ -1,158 +0,0 @@ -package entity - -import ( - "database/sql/driver" - "errors" - "strings" - "time" -) - -type Status string - -type Event struct { - ID int64 - Name string - Description string - StartDate time.Time - EndDate time.Time - Location string - Level string - Included StringArray `gorm:"type:text[]"` - Price float64 - Paid bool - LocationID *int64 - Status Status - CreatedAt time.Time - UpdatedAt time.Time - DeletedAt *time.Time -} - -type StringArray []string - -func (a StringArray) Value() (driver.Value, error) { - if a == nil { - return nil, nil - } - - joined := "{" + strings.Join(a, ",") + "}" - - return []byte(joined), nil -} - -func (a *StringArray) Scan(src interface{}) error { - if src == nil { - *a = nil - return nil - } - - srcStr, ok := src.(string) - if !ok { - return errors.New("failed to scan StringArray") - } - - // Remove the curly braces and split the string into elements - if len(srcStr) < 2 || srcStr[0] != '{' || srcStr[len(srcStr)-1] != '}' { - return errors.New("invalid format for StringArray") - } - srcStr = srcStr[1 : len(srcStr)-1] - - *a = strings.Split(srcStr, ",") - - return nil -} - -type EventSearch struct { - Name string - Limit int - Offset int -} - -type EventList []*EventDB - -type EventDB struct { - Event -} - -func (e *Event) ToEventDB() *EventDB { - return &EventDB{ - Event: *e, - } -} - -func (e *EventDB) ToEvent() *Event { - return &Event{ - ID: e.ID, - Name: e.Name, - Description: e.Description, - StartDate: e.StartDate, - EndDate: e.EndDate, - Location: e.Location, - Level: e.Level, - Included: e.Included, - Price: e.Price, - Paid: e.Paid, - LocationID: e.LocationID, - CreatedAt: e.CreatedAt, - UpdatedAt: e.UpdatedAt, - Status: e.Status, - } -} - -func (e *EventList) ToEventList() []*Event { - var events []*Event - for _, event := range *e { - events = append(events, event.ToEvent()) - } - return events -} - -func (EventDB) TableName() string { - return "events" -} - -func (o *EventDB) ToUpdatedEvent(req Event) { - if req.Name != "" { - o.Name = req.Name - } - - if req.Description != "" { - o.Description = req.Description - } - - if !req.StartDate.IsZero() { - o.StartDate = req.StartDate - } - - if !req.EndDate.IsZero() { - o.EndDate = req.EndDate - } - - if req.Location != "" { - o.Location = req.Location - } - - if req.Level != "" { - o.Level = req.Level - } - - if req.Included != nil && len(req.Included) > 0 { - o.Included = req.Included - } - - if req.Price != 0 { - o.Price = req.Price - } - - if req.LocationID != nil { - o.LocationID = req.LocationID - } - - if req.Status != "" { - o.Status = req.Status - } -} - -func (o *EventDB) SetDeleted() { - currentTime := time.Now() - o.DeletedAt = ¤tTime -} diff --git a/internal/entity/in_progress_order.go b/internal/entity/in_progress_order.go deleted file mode 100644 index 0465c95..0000000 --- a/internal/entity/in_progress_order.go +++ /dev/null @@ -1,29 +0,0 @@ -package entity - -import "time" - -type InProgressOrder struct { - ID string - PartnerID int64 - CustomerID *int64 - CustomerName string - CreatedBy int64 - PaymentType string - PaymentProvider string - OrderItems []InProgressOrderItem - Payment Payment - User User - Source string - OrderType string - TableNumber string - CreatedAt time.Time - UpdatedAt time.Time -} - -type InProgressOrderItem struct { - ID int64 - InProgressOrderID int64 - ItemID int64 - Quantity int - Product *Product -} diff --git a/internal/entity/jwt.go b/internal/entity/jwt.go deleted file mode 100644 index da32770..0000000 --- a/internal/entity/jwt.go +++ /dev/null @@ -1,38 +0,0 @@ -package entity - -import "github.com/golang-jwt/jwt" - -type JWTAuthClaims struct { - UserID int64 `json:"id"` - Name string `json:"name"` - Email string `json:"email"` - Role int `json:"role"` - PartnerID int64 `json:"partner_id"` - SiteID int64 `json:"site_id"` - SiteName string `json:"site_name"` - jwt.StandardClaims -} - -type JWTAuthClaimsCustomer struct { - UserID int64 `json:"id"` - Name string `json:"name"` - Email string `json:"email"` - jwt.StandardClaims -} - -type JWTOrderClaims struct { - PartnerID int64 `json:"id"` - OrderID int64 `json:"order_id"` - InquiryID string `json:"inquiry_id"` - jwt.StandardClaims -} - -type JWTWithdrawClaims struct { - ID int64 `json:"id"` - PartnerID int64 `json:"partner_id"` - OrderID int64 `json:"order_id"` - Amount int64 `json:"amount"` - Fee int64 `json:"fee"` - Total int64 `json:"total"` - jwt.StandardClaims -} diff --git a/internal/entity/license.go b/internal/entity/license.go deleted file mode 100644 index efb2b83..0000000 --- a/internal/entity/license.go +++ /dev/null @@ -1,162 +0,0 @@ -package entity - -import ( - "github.com/google/uuid" - "time" -) - -type License struct { - ID uuid.UUID `gorm:"type:uuid;primaryKey;default:uuid_generate_v4()"` - PartnerID int64 `gorm:"type:bigint;not null"` - Name string `gorm:"type:varchar(255);not null"` - StartDate time.Time `gorm:"type:date;not null"` - EndDate time.Time `gorm:"type:date;not null"` - RenewalDate *time.Time `gorm:"type:date"` - SerialNumber string `gorm:"type:varchar(255);unique;not null"` - CreatedBy int64 `gorm:"type:bigint;not null"` - UpdatedBy int64 `gorm:"type:bigint;not null"` - CreatedAt time.Time `gorm:"autoCreateTime"` - UpdatedAt time.Time `gorm:"autoUpdateTime"` -} - -type LicenseGetAll struct { - ID uuid.UUID `gorm:"type:uuid;primaryKey;default:uuid_generate_v4()"` - PartnerID int64 `gorm:"type:bigint;not null"` - Name string `gorm:"type:varchar(255);not null"` - StartDate time.Time `gorm:"type:date;not null"` - EndDate time.Time `gorm:"type:date;not null"` - RenewalDate *time.Time `gorm:"type:date"` - SerialNumber string `gorm:"type:varchar(255);unique;not null"` - CreatedBy int64 `gorm:"type:bigint;not null"` - UpdatedBy int64 `gorm:"type:bigint;not null"` - CreatedAt time.Time `gorm:"autoCreateTime"` - UpdatedAt time.Time `gorm:"autoUpdateTime"` - PartnerName string `gorm:"type:varchar(255);not null"` - LicenseStatus string `gorm:"type:string(255);not null"` - CreatedByName string `gorm:"type:string(255);not null"` - DaysToExpire int64 `gorm:"type:bigint"` -} - -func (License) TableName() string { - return "licenses" -} - -type LicenseDB struct { - License -} - -func (l *License) ToLicenseDB() *LicenseDB { - return &LicenseDB{ - License: *l, - } -} - -func (LicenseDB) TableName() string { - return "licenses" -} - -func (e *LicenseDB) ToLicense() *License { - return &License{ - ID: e.ID, - PartnerID: e.PartnerID, - Name: e.Name, - StartDate: e.StartDate, - EndDate: e.EndDate, - RenewalDate: e.RenewalDate, - SerialNumber: e.SerialNumber, - CreatedBy: e.CreatedBy, - UpdatedBy: e.UpdatedBy, - CreatedAt: e.CreatedAt, - UpdatedAt: e.UpdatedAt, - } -} - -func (o *LicenseDB) ToUpdatedLicense(updatedBy int64, req License) { - o.UpdatedBy = updatedBy - - if req.Name != "" { - o.Name = req.Name - } - - if !req.StartDate.IsZero() { - o.StartDate = req.StartDate - } - - if !req.EndDate.IsZero() { - o.EndDate = req.EndDate - } - - if req.RenewalDate != nil { - o.RenewalDate = req.RenewalDate - } - - if req.SerialNumber != "" { - o.SerialNumber = req.SerialNumber - } -} - -type PartnerLicense struct { - PartnerID int64 `json:"partner_id"` - LicenseStatus string `json:"license_status"` - DaysToExpire int64 `json:"days_to_expire"` -} - -func (l *LicenseDB) ToPartnerLicense() PartnerLicense { - location, err := time.LoadLocation("Asia/Jakarta") - if err != nil { - location = time.FixedZone("GMT+7", 7*60*60) - } - - // Reinterpret StartDate as GMT+7 without changing the actual time values - startDateInGMT7 := time.Date( - l.StartDate.Year(), - l.StartDate.Month(), - l.StartDate.Day(), - l.StartDate.Hour(), - l.StartDate.Minute(), - l.StartDate.Second(), - l.StartDate.Nanosecond(), - location, - ) - - // Convert EndDate similarly, if needed - endDateInGMT7 := time.Date( - l.EndDate.Year(), - l.EndDate.Month(), - l.EndDate.Day(), - l.EndDate.Hour(), - l.EndDate.Minute(), - l.EndDate.Second(), - l.EndDate.Nanosecond(), - location, - ) - - now := time.Now().In(location) - startOfDay := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) - - daysToExpire := int64(endDateInGMT7.Sub(startOfDay).Hours() / 24) - var licenseStatus string - - if startDateInGMT7.After(startOfDay) { - licenseStatus = "INACTIVE" - } else if startDateInGMT7.Equal(startOfDay) || (startDateInGMT7.Before(startOfDay) && endDateInGMT7.After(startOfDay) || - endDateInGMT7.Equal(startOfDay)) { - if daysToExpire < 0 { - licenseStatus = "EXPIRED" - } else if daysToExpire <= 30 { - licenseStatus = "EXPIRING_SOON" - } else { - licenseStatus = "ACTIVE" - } - } else if endDateInGMT7.Before(startOfDay) { - licenseStatus = "EXPIRED" - } else { - licenseStatus = "ACTIVE" - } - - return PartnerLicense{ - PartnerID: l.PartnerID, - DaysToExpire: daysToExpire, - LicenseStatus: licenseStatus, - } -} diff --git a/internal/entity/linkqu.go b/internal/entity/linkqu.go deleted file mode 100644 index 009b64f..0000000 --- a/internal/entity/linkqu.go +++ /dev/null @@ -1,98 +0,0 @@ -package entity - -type LinkQuRequest struct { - CustomerID string - CustomerName string - CustomerPhone string - CustomerEmail string - PaymentReferenceID string - PaymentMethod string - TotalAmount int64 - BankCode string - OrderItems []OrderItem -} - -type LinkQuCallback struct { - PartnerReff string - PaymentReff string - Status string - Signature string -} - -type LinkQuQRISResponse struct { - Time int `json:"time"` - Amount int64 `json:"amount"` - Expired string `json:"expired"` - CustomerPhone string `json:"customer_phone"` - CustomerID string `json:"customer_id"` - CustomerName string `json:"customer_name"` - CustomerEmail string `json:"customer_email"` - PartnerReff string `json:"partner_reff"` - Username string `json:"username"` - Pin string `json:"pin"` - Status string `json:"status"` - ResponseCode string `json:"response_code"` - ResponseDesc string `json:"response_desc"` - ImageQRIS string `json:"imageqris"` - PartnerReff2 string `json:"partner_reff2"` - FeeAdmin int `json:"feeadmin"` - QRISText string `json:"qris_text"` - Signature string `json:"signature"` - URLCallback string `json:"url_callback"` -} - -type LinkQuPaymentVAResponse struct { - Time int `json:"time"` - Amount int `json:"amount"` - Expired string `json:"expired"` - BankCode string `json:"bank_code"` - BankName string `json:"bank_name"` - CustomerPhone string `json:"customer_phone"` - CustomerID string `json:"customer_id"` - CustomerName string `json:"customer_name"` - CustomerEmail string `json:"customer_email"` - PartnerReff string `json:"partner_reff"` - Username string `json:"username"` - Pin string `json:"pin"` - Status string `json:"status"` - ResponseCode string `json:"response_code"` - ResponseDesc string `json:"response_desc"` - VirtualAccount string `json:"virtual_account"` - PartnerReff2 string `json:"partner_reff2"` - Remark string `json:"remark"` - Signature string `json:"signature"` - UrlCallback string `json:"url_callback"` -} - -type LinkQuCheckStatusResponse struct { - ResponseCode string `json:"rc"` - ResponseDesc string `json:"rd"` - Total int64 `json:"total"` - Balance int64 `json:"balance"` - Data LinkQuCheckStatusData `json:"data"` - Request map[string]interface{} `json:"request"` - LastUpdate string `json:"lastUpdate"` - DataAdditional map[string]interface{} `json:"dataadditional"` -} - -type LinkQuCheckStatusData struct { - InquiryReff int64 `json:"inquiry_reff"` - PaymentReff int64 `json:"payment_reff"` - PartnerReff string `json:"partner_reff"` - Reference string `json:"reference"` - ProductID string `json:"id_produk"` - ProductName string `json:"nama_produk"` - ProductGroup string `json:"grup_produk"` - Debitted int64 `json:"debitted"` - Amount int64 `json:"amount"` - AmountFee int64 `json:"amountfee"` - Info1 string `json:"info1"` - Info2 string `json:"info2"` - Info3 string `json:"info3"` - Info4 string `json:"info4"` - StatusTrx string `json:"status_trx"` - StatusDesc string `json:"status_desc"` - StatusPaid string `json:"status_paid"` - Balance int64 `json:"balance"` - TipsQRIS int64 `json:"tips_qris"` -} diff --git a/internal/entity/member.go b/internal/entity/member.go deleted file mode 100644 index af8d68b..0000000 --- a/internal/entity/member.go +++ /dev/null @@ -1,83 +0,0 @@ -package entity - -import ( - "enaklo-pos-be/internal/constants" - "golang.org/x/crypto/bcrypt" - "time" -) - -type MemberRegistrationRequest struct { - Name string `json:"name" validate:"required"` - Email string `json:"email" validate:"required,email"` - Phone string `json:"phone" validate:"required"` - BirthDate time.Time `json:"birth_date"` - BranchID int64 `json:"branch_id" validate:"required"` - CashierID int64 `json:"cashier_id" validate:"required"` - Password string `json:"password"` -} - -func (m *MemberRegistrationRequest) GetHashPassword() string { - hashedPassword, err := bcrypt.GenerateFromPassword([]byte(m.Password), bcrypt.DefaultCost) - if err != nil { - return "" - } - - return string(hashedPassword) -} - -type MemberRegistrationResponse struct { - Token string `json:"token"` - Status string `json:"status"` - ExpiresAt time.Time `json:"expires_at"` - Message string `json:"message"` -} - -type MemberRegistration struct { - ID string `json:"id"` - Token string `json:"token"` - Name string `json:"name"` - Email string `json:"email"` - Phone string `json:"phone"` - BirthDate time.Time `json:"birth_date"` - OTP string `json:"-"` // Not exposed in JSON responses - Status constants.RegistrationStatus `json:"status"` - ExpiresAt time.Time `json:"expires_at"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at,omitempty"` - BranchID int64 `json:"branch_id"` - CashierID int64 `json:"cashier_id"` - Password string `json:"password"` -} - -type MemberVerificationRequest struct { - Token string `json:"token" validate:"required"` - OTP string `json:"otp" validate:"required"` -} - -type MemberVerificationResponse struct { - Auth *AuthenticateUser - CustomerID int64 `json:"customer_id"` - Name string `json:"name"` - Email string `json:"email"` - Phone string `json:"phone"` - Points int `json:"points"` - Status string `json:"status"` -} - -type MemberRegistrationStatus struct { - Token string `json:"token"` - Status string `json:"status"` - ExpiresAt time.Time `json:"expires_at"` - IsExpired bool `json:"is_expired"` - CreatedAt time.Time `json:"created_at"` -} - -type ResendOTPRequest struct { - Token string `json:"token" validate:"required"` -} - -type ResendOTPResponse struct { - Token string `json:"token"` - ExpiresAt time.Time `json:"expires_at"` - Message string `json:"message"` -} diff --git a/internal/entity/midtrans.go b/internal/entity/midtrans.go deleted file mode 100644 index 4823994..0000000 --- a/internal/entity/midtrans.go +++ /dev/null @@ -1,19 +0,0 @@ -package entity - -type MidtransResponse struct { - Token string - RedirectURL string -} - -type MidtransRequest struct { - PaymentReferenceID string - PaymentMethod string - TotalAmount int64 - OrderItems []OrderItem -} - -type MidtransQrisResponse struct { - QrCodeUrl string - OrderID string - Amount int64 -} diff --git a/internal/entity/order.go b/internal/entity/order.go deleted file mode 100644 index 65692b5..0000000 --- a/internal/entity/order.go +++ /dev/null @@ -1,308 +0,0 @@ -package entity - -import ( - "time" -) - -type Order struct { - ID int64 `gorm:"primaryKey;autoIncrement;column:id"` - PartnerID int64 `gorm:"type:int;column:partner_id"` - Status string `gorm:"type:varchar;column:status"` - Amount float64 `gorm:"type:numeric;not null;column:amount"` - Total float64 `gorm:"type:numeric;not null;column:total"` - Tax float64 `gorm:"type:numeric;not null;column:tax"` - CustomerID *int64 - CustomerName string - InquiryID *string - Site *Site `gorm:"foreignKey:SiteID;constraint:OnDelete:CASCADE;"` - CreatedAt time.Time `gorm:"autoCreateTime;column:created_at"` - UpdatedAt time.Time `gorm:"autoUpdateTime;column:updated_at"` - CreatedBy int64 `gorm:"type:int;column:created_by"` - PaymentType string `gorm:"type:varchar;column:payment_type"` - PaymentProvider string `gorm:"type:varchar;column:payment_provider"` - UpdatedBy int64 `gorm:"type:int;column:updated_by"` - OrderItems []OrderItem `gorm:"foreignKey:OrderID;constraint:OnDelete:CASCADE;"` - Payment Payment `gorm:"foreignKey:OrderID;constraint:OnDelete:CASCADE;"` - User User `gorm:"foreignKey:CreatedBy;constraint:OnDelete:CASCADE;"` - Source string `gorm:"type:varchar;column:source"` - OrderType string `gorm:"type:varchar;column:order_type"` - CashierSessionID int64 `gorm:"type:varchar;column:cashier_session_id"` - TableNumber string - InProgressOrderID int64 -} - -func (o *Order) IsMemberOrder() bool { - return o.CustomerID != nil && *o.CustomerID > 0 -} - -type OrderDB struct { - Order -} - -func (b *Order) ToOrderDB() *OrderDB { - return &OrderDB{ - Order: *b, - } -} - -func (e *OrderDB) ToSumAmount() *Order { - return &Order{ - Amount: e.Amount, - } -} - -type OrderResponse struct { - Order *Order -} - -type CheckinResponse struct { - Order *Order - Token string -} - -type CheckinExecute struct { - Order *Order - Token string -} - -type ExecuteOrderResponse struct { - Order *Order - QRCode string - VirtualAccount string - BankName string - BankCode string - PaymentToken string - RedirectURL string -} - -func (Order) TableName() string { - return "orders" -} - -type OrderItem struct { - ID int64 `gorm:"primaryKey;autoIncrement;column:order_item_id"` - OrderID int64 `gorm:"type:int;column:order_id"` - ItemID int64 `gorm:"type:int;column:item_id"` - ItemType string `gorm:"type:varchar;column:item_type"` - Price float64 `gorm:"type:numeric;not null;column:price"` - Quantity int `gorm:"type:int;column:quantity"` - Status string `gorm:"type:varchar;column:status;default:ACTIVE"` - CreatedAt time.Time `gorm:"autoCreateTime;column:created_at"` - UpdatedAt time.Time `gorm:"autoUpdateTime;column:updated_at"` - CreatedBy int64 `gorm:"type:int;column:created_by"` - UpdatedBy int64 `gorm:"type:int;column:updated_by"` - Product *Product `gorm:"foreignKey:ItemID;references:ID"` - ItemName string `gorm:"type:varchar;column:item_name"` - Notes string `gorm:"type:varchar;column:notes"` -} - -func (OrderItem) TableName() string { - return "order_items" -} - -type OrderRequest struct { - Source string - CreatedBy int64 - PartnerID int64 - PaymentMethod string - OrderItems []OrderItemRequest - CustomerID *int64 - CustomerName string - CustomerEmail string - CustomerPhoneNumber string - TableNumber string - PaymentProvider string - OrderType string - ID int64 - CashierSessionID int64 -} - -type OrderItemRequest struct { - ProductID int64 `json:"product_id" validate:"required"` - Quantity int `json:"quantity" validate:"required"` - Description string `json:"description"` - Notes string `json:"notes"` -} - -type PartialRefundItem struct { - OrderItemID int64 `json:"order_item_id" validate:"required"` - Quantity int `json:"quantity" validate:"required,min=1"` -} - -type VoidItem struct { - OrderItemID int64 `json:"order_item_id" validate:"required"` - Quantity int `json:"quantity" validate:"required,min=1"` -} - -type SplitBillSplit struct { - CustomerName string `json:"customer_name" validate:"required"` - CustomerID *int64 `json:"customer_id"` - Items []SplitBillItem `json:"items,omitempty" validate:"required_if=Type ITEM,dive"` - Amount float64 `json:"amount,omitempty" validate:"required_if=Type AMOUNT,min=0"` -} - -type SplitBillItem struct { - OrderItemID int64 `json:"order_item_id" validate:"required"` - Quantity int `json:"quantity" validate:"required,min=1"` - CustomerName string `json:"customer_name" validate:"required"` - CustomerID *int64 `json:"customer_id"` -} - -type OrderExecuteRequest struct { - CreatedBy int64 - PartnerID int64 - Token string -} - -func (o *Order) SetExecutePaymentStatus() { - o.Status = "PAID" -} - -type CallbackRequest struct { - TransactionStatus string `json:"transaction_status"` - TransactionID string `json:"transaction_id"` -} - -type HistoryOrder struct { - ID int64 `gorm:"primaryKey;autoIncrement;column:id"` - Employee string `gorm:"type:varchar;column:employee"` - Site string `gorm:"type:varchar;column:site"` - Timestamp time.Time `gorm:"autoCreateTime;column:timestamp"` - BookingTime time.Time `gorm:"autoCreateTime;column:booking_time"` - Tickets []string `gorm:"-"` - RawTickets string `gorm:"type:text;column:tickets"` - PaymentType string `gorm:"type:varchar;column:payment_type"` - Status string `gorm:"type:varchar;column:status"` - Amount float64 `gorm:"type:numeric;column:amount"` - VisitDate time.Time `gorm:"type:date;column:visit_date"` - TicketStatus string `gorm:"type:varchar;column:ticket_status"` - Source string `gorm:"type:numeric;column:source"` -} - -func (h *HistoryOrder) GetPaymentStatus() string { - if h.Status == "PAID" { - return "E-TICKET TELAH TERBIT" - } - - if h.Status == "PENDING" { - return "MENUNGGU PEMBAYARAN" - } - - if h.Status == "EXPIRED" { - return "KADALUWARSA" - } - - return "" -} - -type HistoryOrderDB struct { - HistoryOrder -} - -type OrderSearch struct { - PartnerID *int64 - SiteID *int64 - IsAdmin bool - CreatedBy int64 - PaymentType string - Status string - Limit int - Offset int - StartDate string - EndDate string - Period string - IsCustomer bool - Source string -} - -type HistoryOrderList []*HistoryOrderDB - -func (b *HistoryOrder) ToHistoryOrderDB() *HistoryOrderDB { - return &HistoryOrderDB{ - HistoryOrder: *b, - } -} - -func (e *HistoryOrderDB) ToHistoryOrder() *HistoryOrder { - return &HistoryOrder{ - ID: e.ID, - Employee: e.Employee, - Site: e.Site, - Timestamp: e.Timestamp, - BookingTime: e.BookingTime, - Tickets: e.Tickets, - RawTickets: e.RawTickets, - PaymentType: e.PaymentType, - Status: e.Status, - Amount: e.Amount, - VisitDate: e.VisitDate, - TicketStatus: e.TicketStatus, - Source: e.Source, - } -} - -func (b *HistoryOrderList) ToHistoryOrderList() []*HistoryOrder { - var HistoryOrders []*HistoryOrder - for _, historyOrder := range *b { - if historyOrder.Status != "NEW" && historyOrder.Tickets != nil { - HistoryOrders = append(HistoryOrders, historyOrder.ToHistoryOrder()) - } - } - return HistoryOrders -} - -type TicketSold struct { - Count int64 `gorm:"type:int;column:count"` -} - -type TicketSoldDB struct { - TicketSold -} - -func (b *TicketSold) ToTicketSoldDB() *TicketSoldDB { - return &TicketSoldDB{ - TicketSold: *b, - } -} - -func (e *TicketSoldDB) ToTicketSold() *TicketSold { - return &TicketSold{ - Count: e.Count, - } -} - -type ProductDailySales struct { - Day time.Time - SiteID int64 - SiteName string - PaymentType string - Total float64 -} - -type PaymentTypeDistribution struct { - PaymentType string - Count int -} - -type OrderPrintDetail struct { - ID int64 `gorm:"column:id"` - Logo string `gorm:"logo"` - PartnerName string `gorm:"column:partner_name"` - SiteName string `gorm:"column:site_name"` - OrderID string `gorm:"column:order_id"` - VisitDate time.Time `gorm:"column:visit_date"` - PaymentType string `gorm:"column:payment_type"` - OrderItems []OrderItem `gorm:"foreignKey:OrderID;constraint:OnDelete:CASCADE;"` - Source string `gorm:"column:source"` - TicketStatus string `gorm:"column:ticket_status"` - Total float64 `gorm:"column:total"` - Fee float64 `gorm:"column:fee"` -} - -func (o *OrderPrintDetail) GetPaymanetType() string { - if o.PaymentType == "CASH" { - return "TUNAI" - } - - return o.PaymentType -} diff --git a/internal/entity/order_inquiry.go b/internal/entity/order_inquiry.go deleted file mode 100644 index 5cc40e0..0000000 --- a/internal/entity/order_inquiry.go +++ /dev/null @@ -1,135 +0,0 @@ -package entity - -import ( - "enaklo-pos-be/internal/constants" - "time" -) - -type OrderInquiry struct { - ID string `json:"id"` - PartnerID int64 `json:"partner_id"` - CustomerID int64 `json:"customer_id,omitempty"` - CustomerName string `json:"customer_name"` - CustomerPhoneNumber string `json:"customer_phone_number"` - CustomerEmail string `json:"customer_email"` - Status string `json:"status"` - Amount float64 `json:"amount"` - Tax float64 `json:"tax"` - Total float64 `json:"total"` - PaymentType string `json:"payment_type"` - Source string `json:"source"` - CreatedBy int64 `json:"created_by"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - ExpiresAt time.Time `json:"expires_at"` - OrderItems []OrderItem `json:"order_items"` - PaymentProvider string `json:"payment_provider"` - TableNumber string `json:"table_number"` - OrderType string `json:"order_type"` - CashierSessionID int64 `json:"cashier_session_id"` -} - -type OrderCalculation struct { - Subtotal float64 `json:"subtotal"` - Tax float64 `json:"tax"` - Total float64 `json:"total"` -} - -type OrderInquiryResponse struct { - OrderInquiry *OrderInquiry `json:"order_inquiry"` - Token string `json:"token"` -} - -func NewOrderInquiry( - partnerID int64, - customerID int64, - amount float64, - tax float64, - total float64, - paymentType string, - source string, - createdBy int64, - customerName string, - customerPhoneNumber string, - customerEmail string, - paymentProvider string, - tableNumber string, - orderType string, - cashierSessionID int64, -) *OrderInquiry { - return &OrderInquiry{ - ID: constants.GenerateUUID(), - PartnerID: partnerID, - Status: "PENDING", - Amount: amount, - Tax: tax, - Total: total, - PaymentType: paymentType, - CustomerID: customerID, - Source: source, - CreatedBy: createdBy, - CreatedAt: time.Now(), - ExpiresAt: time.Now().Add(2 * time.Minute), - OrderItems: []OrderItem{}, - CustomerName: customerName, - CustomerEmail: customerEmail, - CustomerPhoneNumber: customerPhoneNumber, - PaymentProvider: paymentProvider, - TableNumber: tableNumber, - OrderType: orderType, - CashierSessionID: cashierSessionID, - } -} - -func (oi *OrderInquiry) AddOrderItem(item OrderItemRequest, product *Product) { - oi.OrderItems = append(oi.OrderItems, OrderItem{ - ItemID: item.ProductID, - ItemType: product.Type, - Price: product.Price, - ItemName: product.Name, - Quantity: item.Quantity, - CreatedBy: oi.CreatedBy, - Product: product, - Notes: item.Notes, - }) -} - -func (i *OrderInquiry) ToOrder(paymentMethod, paymentProvider string) *Order { - now := time.Now() - - order := &Order{ - PartnerID: i.PartnerID, - CustomerID: &i.CustomerID, - InquiryID: &i.ID, - Status: constants.StatusPaid, - Amount: i.Amount, - Tax: i.Tax, - Total: i.Total, - PaymentType: paymentMethod, - PaymentProvider: paymentProvider, - Source: i.Source, - CreatedBy: i.CreatedBy, - CreatedAt: now, - OrderItems: make([]OrderItem, len(i.OrderItems)), - OrderType: i.OrderType, - CustomerName: i.CustomerName, - TableNumber: i.TableNumber, - CashierSessionID: i.CashierSessionID, - } - - for idx, item := range i.OrderItems { - order.OrderItems[idx] = OrderItem{ - ItemID: item.ItemID, - ItemType: item.ItemType, - Price: item.Price, - ItemName: item.ItemName, - Quantity: item.Quantity, - CreatedBy: i.CreatedBy, - CreatedAt: now, - Product: item.Product, - Notes: item.Notes, - } - } - - return order -} diff --git a/internal/entity/oss.go b/internal/entity/oss.go deleted file mode 100644 index 0e916db..0000000 --- a/internal/entity/oss.go +++ /dev/null @@ -1,24 +0,0 @@ -package entity - -import "mime/multipart" - -type UploadFileRequest struct { - FileHeader *multipart.FileHeader - FolderName string - FileSize int64 `validate:"max=10000000"` // 10Mb = 10000000 byte - Ext string `validate:"oneof=.png .jpeg .jpg .pdf .xlsx .csv"` -} - -type DownloadFileRequest struct { - FileName string `query:"file_name" validate:"required"` - FolderName string `query:"folder_name" validate:"required"` -} - -type UploadFileResponse struct { - FilePath string `json:"file_path"` - FileUrl string `json:"file_url"` -} - -type DownloadFileResponse struct { - FileUrl string `json:"file_url"` -} diff --git a/internal/entity/partner.go b/internal/entity/partner.go deleted file mode 100644 index e6e4971..0000000 --- a/internal/entity/partner.go +++ /dev/null @@ -1,253 +0,0 @@ -package entity - -import ( - "enaklo-pos-be/internal/constants/role" - "enaklo-pos-be/internal/constants/userstatus" - "time" -) - -type CreatePartnerRequest struct { - Name string `json:"name" validate:"required"` - Address string `json:"address"` - FullName string `json:"full_name"` - Email string `json:"email"` - Password string `json:"password" validate:"required"` - NIK string `json:"nik"` - PhoneNumber string `json:"phone_number"` - BankName string `json:"bank_name"` - BankAccountNumber string `json:"bank_account_number"` - Status string `json:"status"` - BankAccountHolderName string `json:"bank_account_holder_name"` - Logo string `json:"logo"` -} - -type Partner struct { - ID int64 `gorm:"primaryKey;autoIncrement;column:id"` - Name string `gorm:"type:varchar(255);not null;column:name"` - Status string `gorm:"type:varchar(50);column:status"` - LicenseExpiredDate *time.Time `gorm:"type:date;column:license_expired_date"` - Address string `gorm:"type:varchar(255);column:address"` - BankName string `gorm:"type:varchar(255);column:bank_name"` - BankAccountNumber string `gorm:"type:varchar(50);column:bank_account_number"` - BankAccountHolderName string `gorm:"type:varchar(255);column:bank_account_holder_name"` - CreatedAt time.Time `gorm:"autoCreateTime;column:created_at"` - UpdatedAt time.Time `gorm:"autoUpdateTime;column:updated_at"` - DeletedAt *time.Time `gorm:"column:deleted_at"` - CreatedBy int64 `gorm:"type:int;column:created_by"` - UpdatedBy int64 `gorm:"type:int;column:updated_by"` - AdminUserID int64 `gorm:"type:int;column:admin_user_id"` - Balance float64 `gorm:"-"` - AdminName string `gorm:"-"` - AdminPhoneNumber string `gorm:"-"` - AdminEmail string `gorm:"-"` - Logo string `gorm:"type:varchar;column:logo"` -} - -type PartnerUpdate struct { - ID int64 - Email string - Name string - Status string - Address string - PhoneNumber string - BankName string - BankAccountNumber string - BankAccountHolderName string - NIK string - AdminUserID int64 - AdminName string - Password string - Logo string -} - -func (c *PartnerUpdate) ToUserAdmin(partnerID *int64) *User { - return &User{ - ID: c.AdminUserID, - Name: c.Name, - Password: c.Password, - Email: c.Email, - NIK: c.NIK, - PhoneNumber: c.PhoneNumber, - Status: userstatus.UserStatus(c.Status), - PartnerID: partnerID, - } -} - -func (Partner) TableName() string { - return "partners" -} - -type PartnerSearch struct { - Search string - PartnerID *int64 - Name string - Limit int - Offset int -} - -type PartnerList []*PartnerDB - -type PartnerDB struct { - Partner -} - -type PartnerDBSearch struct { - Partner - WalletBalance float64 `gorm:"type:number;column:wallet_balance"` - AdminName string `gorm:"type:varchar;column:admin_name"` - AdminEmail string `gorm:"type:varchar;column:admin_email"` - AdminPhoneNumber string `gorm:"type:varchar;column:admin_phone_number"` -} - -func (p *Partner) ToPartnerDB() *PartnerDB { - return &PartnerDB{ - Partner: *p, - } -} - -func (PartnerDB) TableName() string { - return "partners" -} - -func (e *PartnerDB) ToPartner() *Partner { - return &Partner{ - ID: e.ID, - Name: e.Name, - Status: e.Status, - Address: e.Address, - CreatedAt: e.CreatedAt, - UpdatedAt: e.UpdatedAt, - CreatedBy: e.CreatedBy, - Balance: e.Balance, - AdminEmail: e.AdminEmail, - AdminPhoneNumber: e.AdminPhoneNumber, - AdminName: e.AdminName, - BankAccountHolderName: e.BankAccountHolderName, - BankName: e.BankName, - BankAccountNumber: e.BankAccountNumber, - Logo: e.Logo, - } -} - -func (p *PartnerList) ToPartnerList() []*Partner { - var partners []*Partner - for _, partner := range *p { - partners = append(partners, partner.ToPartner()) - } - return partners -} - -func (o *PartnerDB) ToUpdatedPartner(updatedBy int64, req Partner) { - o.UpdatedBy = updatedBy - - if req.Name != "" { - o.Name = req.Name - } - - if req.Status != "" { - o.Status = req.Status - } - - if req.Address != "" { - o.Address = req.Address - } - - if req.BankAccountNumber != "" { - o.BankAccountNumber = req.BankAccountNumber - } - - if req.BankAccountHolderName != "" { - o.BankAccountHolderName = req.BankAccountHolderName - } - - if req.Status != "" { - o.Status = req.Status - } -} - -func (o *PartnerDB) ToUpdatedPartnerData(updatedBy int64, req PartnerUpdate) { - o.UpdatedBy = updatedBy - - if req.Name != "" { - o.Name = req.Name - } - - if req.Status != "" { - o.Status = req.Status - } - - if req.Address != "" { - o.Address = req.Address - } - - if req.BankName != "" { - o.BankName = req.BankName - } - - if req.BankAccountNumber != "" { - o.BankAccountNumber = req.BankAccountNumber - } - - if req.BankAccountHolderName != "" { - o.BankAccountHolderName = req.BankAccountHolderName - } - - if req.Status != "" { - o.Status = req.Status - } - - if req.Logo != "" { - o.Logo = req.Logo - } -} - -func (o *PartnerDB) SetDeleted(updatedBy int64) { - currentTime := time.Now() - o.DeletedAt = ¤tTime - o.UpdatedBy = updatedBy -} - -func (c *CreatePartnerRequest) ToUserAdmin(partnerID int64) *User { - return &User{ - Name: c.FullName, - Password: c.Password, - Email: c.Email, - NIK: c.NIK, - PhoneNumber: c.PhoneNumber, - Status: "Active", - RoleID: role.PartnerAdmin, - PartnerID: &partnerID, - } -} - -func (e *CreatePartnerRequest) ToPartnerDB(createdBy int64) *PartnerDB { - twoDays := 48 * time.Hour - licenseExpiredDate := time.Now().Add(twoDays) - - return &PartnerDB{ - Partner: Partner{ - Name: e.Name, - Status: e.Status, - Address: e.Address, - CreatedBy: createdBy, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - BankAccountHolderName: e.BankAccountHolderName, - BankAccountNumber: e.BankAccountNumber, - BankName: e.BankName, - LicenseExpiredDate: &licenseExpiredDate, - Logo: e.Logo, - }, - } -} - -func (e *CreatePartnerRequest) ToWallet(partnerID int64) *Wallet { - return &Wallet{ - PartnerID: partnerID, - Balance: 0, - Currency: "IDR", - Status: "active", - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - } -} diff --git a/internal/entity/partner_setting.go b/internal/entity/partner_setting.go deleted file mode 100644 index 8e9e6bd..0000000 --- a/internal/entity/partner_setting.go +++ /dev/null @@ -1,56 +0,0 @@ -package entity - -import ( - "time" -) - -type PartnerSettings struct { - PartnerID int64 `json:"partner_id"` - TaxEnabled bool `json:"tax_enabled"` - TaxPercentage float64 `json:"tax_percentage"` - InvoicePrefix string `json:"invoice_prefix"` - BusinessHours string `json:"business_hours"` - LogoURL string `json:"logo_url"` - ThemeColor string `json:"theme_color"` - ReceiptFooterText string `json:"receipt_footer_text"` - ReceiptHeaderText string `json:"receipt_header_text"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} - -type PartnerPaymentMethod struct { - ID int64 `json:"id"` - PartnerID int64 `json:"partner_id"` - PaymentMethod string `json:"payment_method"` - IsEnabled bool `json:"is_enabled"` - DisplayName string `json:"display_name"` - DisplayOrder int `json:"display_order"` - AdditionalInfo string `json:"additional_info"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} - -type PartnerFeatureFlag struct { - ID int64 `json:"id"` - PartnerID int64 `json:"partner_id"` - FeatureKey string `json:"feature_key"` - IsEnabled bool `json:"is_enabled"` - Config string `json:"config"` // JSON string - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} - -type BusinessHoursSetting struct { - Monday DayHours `json:"monday"` - Tuesday DayHours `json:"tuesday"` - Wednesday DayHours `json:"wednesday"` - Thursday DayHours `json:"thursday"` - Friday DayHours `json:"friday"` - Saturday DayHours `json:"saturday"` - Sunday DayHours `json:"sunday"` -} - -type DayHours struct { - Open string `json:"open"` // Format: "HH:MM" - Close string `json:"close"` // Format: "HH:MM" -} diff --git a/internal/entity/payment.go b/internal/entity/payment.go deleted file mode 100644 index 39c21be..0000000 --- a/internal/entity/payment.go +++ /dev/null @@ -1,26 +0,0 @@ -package entity - -import ( - "github.com/google/uuid" - "gorm.io/datatypes" - "time" -) - -type Payment struct { - ID uuid.UUID `gorm:"type:uuid;default:uuid_generate_v4();primaryKey;column:id"` - PartnerID int64 `gorm:"type:numeric;not null;column:partner_id"` - OrderID int64 `gorm:"type:numeric;not null;column:order_id"` - ReferenceID string `gorm:"type:varchar;not null;column:reference_id"` - Channel string `gorm:"type:varchar;not null;column:channel"` - PaymentType string `gorm:"type:varchar;not null;column:payment_type"` - Amount float64 `gorm:"type:numeric;not null;column:amount"` - State string `gorm:"type:varchar;not null;column:state"` - RequestMetadata datatypes.JSON `gorm:"type:json;not null;column:request_metadata"` - CreatedAt time.Time `gorm:"autoCreateTime;column:created_at"` - UpdatedAt time.Time `gorm:"autoUpdateTime;column:updated_at"` - FinishedAt time.Time `gorm:"column:finished_at"` -} - -func (Payment) TableName() string { - return "payments" -} diff --git a/internal/entity/payment_gateway.go b/internal/entity/payment_gateway.go deleted file mode 100644 index ca903e6..0000000 --- a/internal/entity/payment_gateway.go +++ /dev/null @@ -1,23 +0,0 @@ -package entity - -type PaymentRequest struct { - PaymentReferenceID string - Provider string - TotalAmount int64 - CustomerID string - CustomerName string - CustomerPhone string - CustomerEmail string - BankCode string -} - -type PaymentResponse struct { - Token string - RedirectURL string - QRCodeURL string - OrderID string - Amount int64 - VirtualAccountNumber string - BankName string - BankCode string -} diff --git a/internal/entity/product.go b/internal/entity/product.go deleted file mode 100644 index fb029e9..0000000 --- a/internal/entity/product.go +++ /dev/null @@ -1,177 +0,0 @@ -package entity - -import ( - "enaklo-pos-be/internal/constants/product" - "enaklo-pos-be/internal/repository/models" - "time" -) - -type Product struct { - ID int64 `gorm:"primaryKey;autoIncrement;column:id"` - PartnerID int64 `gorm:"type:int;column:partner_id"` - Name string `gorm:"type:varchar(255);not null;column:name"` - Type string `gorm:"type:varchar;column:type"` - Price float64 `gorm:"type:decimal;column:price"` - Status string `gorm:"type:varchar;column:status"` - Description string `gorm:"type:varchar(255);not null;column:description"` - CreatedAt time.Time `gorm:"autoCreateTime;column:created_at"` - UpdatedAt time.Time `gorm:"autoUpdateTime;column:updated_at"` - DeletedAt *time.Time `gorm:"column:deleted_at"` - CreatedBy int64 `gorm:"type:int;column:created_by"` - UpdatedBy int64 `gorm:"type:int;column:updated_by"` - Image string `gorm:"type:varchar;column:image"` - CategoryID *int64 `gorm:"column:category_id"` - Category *models.CategoryDB `gorm:"foreignKey:CategoryID;references:ID"` -} - -func (Product) TableName() string { - return "products" -} - -type ProductSearch struct { - Search string - Name string - Type product.ProductType - BranchID int64 - PartnerID int64 - Available product.ProductStock - Limit int - Offset int - CategoryID int64 -} - -type ProductPOS struct { - PartnerID int64 - SiteID int64 -} - -type ProductList []*ProductDB - -type ProductDB struct { - Product -} - -func (b *Product) ToProductDB() *ProductDB { - return &ProductDB{ - Product: *b, - } -} - -func (ProductDB) TableName() string { - return "products" -} - -func (e *ProductDB) ToProduct() *Product { - return &Product{ - ID: e.ID, - Name: e.Name, - Type: e.Type, - Price: e.Price, - Status: e.Status, - Description: e.Description, - PartnerID: e.PartnerID, - CreatedAt: e.CreatedAt, - UpdatedAt: e.UpdatedAt, - DeletedAt: e.DeletedAt, - CreatedBy: e.CreatedBy, - UpdatedBy: e.UpdatedBy, - Image: e.Image, - Category: e.Category, - CategoryID: e.CategoryID, - } -} - -func (b *ProductList) ToProductList() []*Product { - var Products []*Product - - for _, p := range *b { - Products = append(Products, p.ToProduct()) - } - - return Products -} - -func (b *ProductList) ToProductListPOS() []*Product { - var Products []*Product - - for _, p := range *b { - Products = append(Products, p.ToProduct()) - } - - return Products -} - -func (o *ProductDB) ToUpdatedProduct(updatedby int64, req Product) { - o.UpdatedBy = updatedby - - if req.Name != "" { - o.Name = req.Name - } - - if req.Image != "" { - o.Image = req.Image - } - - if req.Type != "" { - o.Type = req.Type - } - - if req.Price > 0 { - o.Price = req.Price - } - - if req.Status != "" { - o.Status = req.Status - } - - if req.Description != "" { - o.Description = req.Description - } -} - -func (o *ProductDB) SetDeleted(updatedby int64) { - currentTime := time.Now() - o.DeletedAt = ¤tTime - o.UpdatedBy = updatedby -} - -type ProductDetails struct { - Products map[int64]*Product // Map for quick lookups by ID - PartnerID int64 // Common site ID for all products -} - -type PaymentMethodBreakdown struct { - PaymentType string `json:"payment_type"` - PaymentProvider string `json:"payment_provider"` - TotalTransactions int64 `json:"total_transactions"` - TotalAmount float64 `json:"total_amount"` -} - -type OrderPaymentAnalysis struct { - TotalTransactions int64 `json:"total"` - TotalAmount float64 `json:"total_amount"` - PaymentMethodBreakdown []PaymentMethodBreakdown `json:"payment_method_breakdown"` -} - -type RevenueOverviewItem struct { - Period string `json:"period"` - TotalAmount float64 `json:"total_amount"` - OrderCount int64 `json:"order_count"` -} - -type SalesByCategoryItem struct { - Category string `json:"category"` - TotalAmount float64 `json:"total_amount"` - TotalQuantity int64 `json:"total_quantity"` - Percentage float64 `json:"percentage"` -} - -type PopularProductItem struct { - ProductID int64 `json:"product_id"` - ProductName string `json:"product_name"` - Category string `json:"category"` - TotalSales int64 `json:"total_sales"` - TotalRevenue float64 `json:"total_revenue"` - AveragePrice float64 `json:"average_price"` - Percentage float64 `json:"percentage"` -} diff --git a/internal/entity/search.go b/internal/entity/search.go deleted file mode 100644 index b0a12f8..0000000 --- a/internal/entity/search.go +++ /dev/null @@ -1,32 +0,0 @@ -package entity - -import "time" - -type SearchRequest struct { - Status string // Filter by order status (e.g., "COMPLETED", "PENDING", etc.) - Start time.Time // Start date for filtering orders - End time.Time // End date for filtering orders - Limit int // Maximum number of records to return - Offset int // Number of records to skip for pagination -} - -type RevenueOverviewRequest struct { - PartnerID int64 - Year int - Granularity string // "monthly" or "weekly" - Status string -} - -type SalesByCategoryRequest struct { - PartnerID int64 - Period string // "d" (daily), "w" (weekly), "m" (monthly) - Status string -} - -type PopularProductsRequest struct { - PartnerID int64 - Period string // "d" (daily), "w" (weekly), "m" (monthly) - Status string - Limit int - SortBy string // "sales" or "revenue" -} diff --git a/internal/entity/sites.go b/internal/entity/sites.go deleted file mode 100644 index 465a492..0000000 --- a/internal/entity/sites.go +++ /dev/null @@ -1,210 +0,0 @@ -package entity - -import ( - "time" -) - -type Site struct { - ID int64 `gorm:"primaryKey;autoIncrement;column:id"` - Name string `gorm:"type:varchar(255);not null;column:name"` - PartnerID int64 `gorm:"type:int;column:partner_id"` - Image string `gorm:"type:varchar;column:image"` - Address string `gorm:"type:varchar;column:address"` - LocationLink string `gorm:"type:varchar;column:location_link"` - Description string `gorm:"type:varchar;column:description"` - Highlight string `gorm:"type:varchar;column:highlight"` - ContactPerson string `gorm:"type:varchar;column:contact_person"` - TnC string `gorm:"type:varchar;column:tnc"` - AdditionalInfo string `gorm:"type:varchar;column:additional_info"` - Status string `gorm:"type:varchar;column:status"` - IsSeasonTicket bool `gorm:"type:bool;column:is_season_ticket"` - IsDiscountActive bool `gorm:"type:bool;column:is_discount_active"` - CreatedAt time.Time `gorm:"autoCreateTime;column:created_at"` - UpdatedAt time.Time `gorm:"autoUpdateTime;column:updated_at"` - DeletedAt *time.Time `gorm:"column:deleted_at"` - CreatedBy int64 `gorm:"type:int;column:created_by"` - UpdatedBy int64 `gorm:"type:int;column:updated_by"` - Products []Product `gorm:"foreignKey:SiteID;constraint:OnDelete:CASCADE;"` - Latitude *float64 `json:"latitude"` - Longitude *float64 `json:"longitude"` - Region string `json:"region"` - Regency string `json:"regency"` - Distance float64 `gorm:"-"` -} - -type SiteSearch struct { - PartnerID *int64 - SiteID *int64 - IsAdmin bool - Search string - Name string - Limit int - Offset int - Status string -} - -type SiteList []*SiteDB - -type SiteDB struct { - Site -} - -func (s *Site) ToSiteDB() *SiteDB { - return &SiteDB{ - Site: *s, - } -} - -func (SiteDB) TableName() string { - return "sites" -} - -func (e *SiteDB) ToSite() *Site { - return &Site{ - ID: e.ID, - Name: e.Name, - PartnerID: e.PartnerID, - Image: e.Image, - Address: e.Address, - LocationLink: e.LocationLink, - Description: e.Description, - Highlight: e.Highlight, - ContactPerson: e.ContactPerson, - TnC: e.TnC, - AdditionalInfo: e.AdditionalInfo, - Status: e.Status, - IsSeasonTicket: e.IsSeasonTicket, - IsDiscountActive: e.IsDiscountActive, - CreatedAt: e.CreatedAt, - UpdatedAt: e.UpdatedAt, - DeletedAt: e.DeletedAt, - CreatedBy: e.CreatedBy, - UpdatedBy: e.UpdatedBy, - Regency: e.Regency, - Region: e.Region, - Latitude: e.Latitude, - Longitude: e.Longitude, - Products: e.Products, - } -} - -func (s *SiteList) ToSiteList() []*Site { - var sites []*Site - for _, site := range *s { - sites = append(sites, site.ToSite()) - } - return sites -} - -func (o *SiteDB) ToUpdatedSite(updatedBy int64, req Site) { - o.UpdatedBy = updatedBy - - if req.Name != "" { - o.Name = req.Name - } - - if req.PartnerID != 0 { - o.PartnerID = req.PartnerID - } - - if req.Image != "" { - o.Image = req.Image - } - - if req.Address != "" { - o.Address = req.Address - } - - if req.LocationLink != "" { - o.LocationLink = req.LocationLink - } - - if req.Description != "" { - o.Description = req.Description - } - - if req.Highlight != "" { - o.Highlight = req.Highlight - } - - if req.ContactPerson != "" { - o.ContactPerson = req.ContactPerson - } - - if req.TnC != "" { - o.TnC = req.TnC - } - - if req.AdditionalInfo != "" { - o.AdditionalInfo = req.AdditionalInfo - } - - if req.Status != "" { - o.Status = req.Status - } - - if req.IsSeasonTicket { - o.IsSeasonTicket = req.IsSeasonTicket - } - - if req.IsDiscountActive { - o.IsDiscountActive = req.IsDiscountActive - } -} - -func (o *SiteDB) SetDeleted(updatedBy int64) { - currentTime := time.Now() - o.DeletedAt = ¤tTime - o.UpdatedBy = updatedBy -} - -type SiteCount struct { - Count int `gorm:"type:int;column:count"` -} - -type SiteCountDB struct { - SiteCount -} - -func (b *SiteCount) ToSiteCountDB() *SiteCountDB { - return &SiteCountDB{ - SiteCount: *b, - } -} - -func (e *SiteCountDB) ToSiteCount() *SiteCount { - return &SiteCount{ - Count: e.Count, - } -} - -type SiteProductInfo struct { - SiteID int64 `json:"site_id"` - SiteName string `json:"site_name"` - PartnerID int64 `json:"partner_id"` - Image string `json:"image"` - Address string `json:"address"` - LocationLink string `json:"location_link"` - Description string `json:"description"` - Highlight string `json:"highlight"` - ContactPerson string `json:"contact_person"` - TnC string `json:"tnc"` - AdditionalInfo string `json:"additional_info"` - Status string `json:"status"` - IsSeasonTicket bool `json:"is_season_ticket"` - IsDiscountActive bool `json:"is_discount_active"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - Latitude float64 `json:"latitude"` - Longitude float64 `json:"longitude"` - Distance float64 `json:"distance"` // Calculated field - ProductID int64 `json:"product_id"` - ProductName string `json:"product_name"` - ProductType string `json:"product_type"` - ProductPrice float64 `json:"product_price"` - IsWeekendTicket bool `json:"is_weekend_ticket"` - ProductStatus string `json:"product_status"` - ProductDescription string `json:"product_description"` - Region string `json:"region"` - Regency string `json:"regency"` -} diff --git a/internal/entity/studio.go b/internal/entity/studio.go deleted file mode 100644 index 0e8762f..0000000 --- a/internal/entity/studio.go +++ /dev/null @@ -1,95 +0,0 @@ -package entity - -import ( - "enaklo-pos-be/internal/constants/studio" - "time" -) - -type Studio struct { - ID int64 - BranchId int64 - Name string - Status studio.StudioStatus - Price float64 - Metadata []byte `gorm:"type:jsonb"` // Use jsonb data type for JSON data - CreatedAt time.Time - UpdatedAt time.Time - CreatedBy int64 - UpdatedBy int64 -} - -func (s *Studio) TableName() string { - return "studios" -} - -func (s *Studio) NewStudiosDB() *StudioDB { - return &StudioDB{ - Studio: *s, - } -} - -type StudioList []*StudioDB - -type StudioDB struct { - Studio -} - -func (s *StudioDB) ToStudio() *Studio { - return &Studio{ - ID: s.ID, - BranchId: s.BranchId, - Name: s.Name, - Status: s.Status, - Price: s.Price, - Metadata: s.Metadata, - CreatedAt: s.CreatedAt, - UpdatedAt: s.UpdatedAt, - CreatedBy: s.CreatedBy, - UpdatedBy: s.UpdatedBy, - } -} - -func (s *StudioList) ToStudioList() []*Studio { - var studios []*Studio - for _, studio := range *s { - studios = append(studios, studio.ToStudio()) - } - return studios -} - -func (s *StudioDB) ToUpdatedStudio(updatedBy int64, req Studio) { - s.UpdatedBy = updatedBy - - if req.BranchId != 0 { - s.BranchId = req.BranchId - } - - if req.Name != "" { - s.Name = req.Name - } - - if req.Status != "" { - s.Status = req.Status - } - - if req.Price != 0 { - s.Price = req.Price - } - - if req.Metadata != nil { - s.Metadata = req.Metadata - } -} - -func (s *StudioDB) ToStudioDB() *StudioDB { - return s -} - -type StudioSearch struct { - Id int64 - Name string - Status studio.StudioStatus - BranchId int64 - Limit int - Offset int -} diff --git a/internal/entity/transaction.go b/internal/entity/transaction.go deleted file mode 100644 index 00f2f7e..0000000 --- a/internal/entity/transaction.go +++ /dev/null @@ -1,62 +0,0 @@ -package entity - -import ( - "time" -) - -type Transaction struct { - ID string `gorm:"type:uuid;primaryKey;default:uuid_generate_v4()"` - OrderID int64 - PartnerID int64 `gorm:"not null"` - TransactionType string `gorm:"not null"` - Status string `gorm:"size:255"` - CreatedBy int64 `gorm:"not null"` - UpdatedBy int64 `gorm:"not null"` - Amount float64 `gorm:"not null"` - CreatedAt time.Time `gorm:"autoCreateTime"` - UpdatedAt time.Time `gorm:"autoUpdateTime"` - PaymentMethod string `json:"payment_method"` - Fee float64 - Total float64 -} - -type TransactionDB struct { - Transaction -} - -func (b *Transaction) ToTransactionDB() *TransactionDB { - return &TransactionDB{ - Transaction: *b, - } -} - -func (TransactionDB) TableName() string { - return "transactions" -} - -type TransactionSearch struct { - PartnerID *int64 - SiteID *int64 - Type string - Status string - Limit int - Offset int - Date string -} - -type TransactionList struct { - ID string - TransactionType string - Status string - CreatedAt time.Time - SiteName string - PartnerName string - Amount int64 - Total int64 - Fee int64 -} - -type TransactionApproval struct { - TransactionID string - Status string -} diff --git a/internal/entity/undian.go b/internal/entity/undian.go deleted file mode 100644 index 6139192..0000000 --- a/internal/entity/undian.go +++ /dev/null @@ -1,118 +0,0 @@ -package entity - -import "time" - -type UndianEventDB struct { - ID int64 `gorm:"primaryKey;autoIncrement" json:"id"` - Title string `gorm:"size:255;not null" json:"title"` - Description *string `gorm:"type:text" json:"description"` - ImageURL *string `gorm:"size:500" json:"image_url"` - Status string `gorm:"size:20;not null;default:upcoming" json:"status"` - StartDate time.Time `gorm:"not null" json:"start_date"` - EndDate time.Time `gorm:"not null" json:"end_date"` - DrawDate time.Time `gorm:"not null" json:"draw_date"` - MinimumPurchase float64 `gorm:"type:numeric(10,2);default:50000" json:"minimum_purchase"` - DrawCompleted bool `gorm:"default:false" json:"draw_completed"` - DrawCompletedAt *time.Time `json:"draw_completed_at"` - TermsAndConditions *string `gorm:"column:terms_and_conditions;type:text" json:"terms_and_conditions"` - CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` - UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` - Prefix *string `json:"prefix"` - Prizes []UndianPrizeDB `gorm:"foreignKey:UndianEventID" json:"prizes,omitempty"` - Vouchers []UndianVoucherDB `gorm:"foreignKey:UndianEventID" json:"vouchers,omitempty"` -} - -func (UndianEventDB) TableName() string { - return "undian_events" -} - -// UndianPrizeDB represents the undian_prizes table -type UndianPrizeDB struct { - ID int64 `gorm:"primaryKey;autoIncrement" json:"id"` - UndianEventID int64 `gorm:"not null" json:"undian_event_id"` - Rank int `gorm:"not null" json:"rank"` - PrizeName string `gorm:"size:255;not null" json:"prize_name"` - PrizeValue *float64 `gorm:"type:numeric(15,2)" json:"prize_value"` - PrizeDescription *string `gorm:"type:text" json:"prize_description"` - PrizeType string `gorm:"size:50;default:voucher" json:"prize_type"` - PrizeImageURL *string `gorm:"size:500" json:"prize_image_url"` - WinningVoucherID *int64 `json:"winning_voucher_id"` - WinnerUserID *int64 `json:"winner_user_id"` - Amount *int64 `json:"amount"` - // Relations - UndianEvent UndianEventDB `gorm:"foreignKey:UndianEventID" json:"undian_event,omitempty"` -} - -func (UndianPrizeDB) TableName() string { - return "undian_prizes" -} - -// UndianVoucherDB represents the undian_vouchers table -type UndianVoucherDB struct { - ID int64 `gorm:"primaryKey;autoIncrement" json:"id"` - UndianEventID int64 `gorm:"not null" json:"undian_event_id"` - CustomerID int64 `gorm:"not null" json:"customer_id"` - OrderID *int64 `json:"order_id"` - VoucherCode string `gorm:"size:50;not null;uniqueIndex" json:"voucher_code"` - VoucherNumber *int `json:"voucher_number"` - IsWinner bool `gorm:"default:false" json:"is_winner"` - PrizeRank *int `json:"prize_rank"` - WonAt *time.Time `json:"won_at"` - CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` - - // Relations - UndianEvent UndianEventDB `gorm:"foreignKey:UndianEventID" json:"undian_event,omitempty"` -} - -func (UndianVoucherDB) TableName() string { - return "undian_vouchers" -} - -// Response Models -type UndianListResponse struct { - Events []*UndianEventResponse `json:"events"` -} - -type UndianEventResponse struct { - ID int64 `json:"id"` - Title string `json:"title"` - Description *string `json:"description"` - ImageURL *string `json:"image_url"` - Status string `json:"status"` - StartDate time.Time `json:"start_date"` - EndDate time.Time `json:"end_date"` - DrawDate time.Time `json:"draw_date"` - MinimumPurchase float64 `json:"minimum_purchase"` - DrawCompleted bool `json:"draw_completed"` - DrawCompletedAt *time.Time `json:"draw_completed_at"` - TermsConditions *string `json:"terms_and_conditions"` - Prefix *string `json:"prefix"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - VoucherCount int `json:"voucher_count"` - Vouchers []*UndianVoucherResponse `json:"vouchers"` - Prizes []*UndianPrizeResponse `json:"prizes"` -} - -type UndianVoucherResponse struct { - ID int64 `json:"id"` - VoucherCode string `json:"voucher_code"` - VoucherNumber *int `json:"voucher_number"` - IsWinner bool `json:"is_winner"` - PrizeRank *int `json:"prize_rank"` - WonAt *time.Time `json:"won_at"` - CreatedAt time.Time `json:"created_at"` -} - -type UndianPrizeResponse struct { - ID int64 `json:"id"` - Rank int `json:"rank"` - PrizeName string `json:"prize_name"` - PrizeValue *float64 `json:"prize_value"` - PrizeDescription *string `json:"prize_description"` - PrizeType string `json:"prize_type"` - PrizeImageURL *string `json:"prize_image_url"` - WinningVoucherID *int64 `json:"winning_voucher_id"` - WinnerUserID *int64 `json:"winner_user_id"` - Amount *int64 `json:"amount"` -} diff --git a/internal/entity/user.go b/internal/entity/user.go deleted file mode 100644 index d951ce7..0000000 --- a/internal/entity/user.go +++ /dev/null @@ -1,143 +0,0 @@ -package entity - -import ( - "enaklo-pos-be/internal/constants/role" - "enaklo-pos-be/internal/constants/userstatus" - "errors" - "time" - - "golang.org/x/crypto/bcrypt" -) - -type User struct { - ID int64 - Name string - Email string - Password string - Status userstatus.UserStatus - NIK string - UserType string - CreatedAt time.Time - UpdatedAt time.Time - RoleID role.Role - PhoneNumber string - RoleName string - PartnerID *int64 - SiteID *int64 - SiteName string - PartnerName string - ResetPassword bool -} - -type Customer struct { - ID int64 - Name string - Email string - Password string - Phone string - Points int - Status userstatus.UserStatus - NIK string - UserType string - CreatedAt time.Time - UpdatedAt time.Time - RoleID role.Role - PhoneNumber string - RoleName string - PartnerID *int64 - SiteID *int64 - SiteName string - PartnerName string - ResetPassword bool - CustomerID string - BirthDate time.Time - VerificationID string - OTP string -} - -type CustomerPoints struct { - ID uint64 - CustomerID uint64 - TotalPoints int - AvailablePoints int -} - -type AuthenticateUser struct { - ID int64 - Token string - Name string - RoleID role.Role - RoleName string - PartnerID *int64 - PartnerName string - PartnerStatus string - SiteID *int64 - SiteName string - ResetPassword bool - PartnerLicense PartnerLicense - UserType string -} - -type UserRoleDB struct { - ID int64 `gorm:"primary_key;column:user_role_id" ` - UserID int64 `gorm:"column:user_id"` - RoleID int64 `gorm:"column:role_id"` - PartnerID *int64 `gorm:"column:partner_id"` - SiteID *int64 `gorm:"column:site_id"` - CreatedAt time.Time `gorm:"column:created_at"` - UpdatedAt time.Time `gorm:"column:updated_at"` -} - -func (UserRoleDB) TableName() string { - return "user_roles" -} - -func (u *User) ToUserDB(createdBy int64) (*UserDB, error) { - hashedPassword, err := u.HashedPassword(u.Password) - - if err != nil { - return nil, err - } - - if u.RoleID == role.Admin && u.PartnerID == nil { - return nil, errors.New("invalid request") - } - - return &UserDB{ - Name: u.Name, - Email: u.Email, - Password: hashedPassword, - RoleID: int64(u.RoleID), - PartnerID: u.PartnerID, - Status: userstatus.Active, - CreatedBy: createdBy, - SiteID: u.SiteID, - PhoneNumber: u.PhoneNumber, - NIK: u.NIK, - UserType: u.UserType, - }, nil -} - -func (u User) HashedPassword(password string) (string, error) { - hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) - if err != nil { - return "", err - } - - return string(hashedPassword), nil -} - -func (c Customer) HashedPassword() string { - hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(c.Password), bcrypt.DefaultCost) - - return string(hashedPassword) -} - -func (u *Customer) ToUserAuthenticate(signedToken string) *AuthenticateUser { - return &AuthenticateUser{ - ID: u.ID, - Token: signedToken, - Name: u.Name, - UserType: u.UserType, - } -} diff --git a/internal/entity/wallet.go b/internal/entity/wallet.go deleted file mode 100644 index e7e1b5d..0000000 --- a/internal/entity/wallet.go +++ /dev/null @@ -1,27 +0,0 @@ -package entity - -import "time" - -type Wallet struct { - ID int64 `gorm:"primaryKey;autoIncrement;column:id"` - PartnerID int64 `gorm:"type:int;not null;column:partner_id"` - Balance float64 `gorm:"type:decimal(18,2);not null;default:0.00;column:balance"` - AuthBalance float64 `gorm:"type:decimal(18,2);not null;default:0.00;column:auth_balance"` - Currency string `gorm:"type:varchar(3);not null;column:currency"` - Status string `gorm:"type:varchar(50);column:status"` - CreatedAt time.Time `gorm:"autoCreateTime;column:created_at"` - UpdatedAt time.Time `gorm:"autoUpdateTime;column:updated_at"` -} - -func (Wallet) TableName() string { - return "wallets" -} - -type WalletWithdrawRequest struct { - ID int64 - Token string - PartnerID int64 - Amount int64 - Fee int64 - Total int64 -} diff --git a/internal/handler/analytics_handler.go b/internal/handler/analytics_handler.go new file mode 100644 index 0000000..5464620 --- /dev/null +++ b/internal/handler/analytics_handler.go @@ -0,0 +1,157 @@ +package handler + +import ( + "apskel-pos-be/internal/appcontext" + "apskel-pos-be/internal/contract" + "apskel-pos-be/internal/service" + "apskel-pos-be/internal/transformer" + "apskel-pos-be/internal/util" + + "github.com/gin-gonic/gin" +) + +type AnalyticsHandler struct { + analyticsService service.AnalyticsService + transformer transformer.Transformer +} + +func NewAnalyticsHandler( + analyticsService service.AnalyticsService, + transformer transformer.Transformer, +) *AnalyticsHandler { + return &AnalyticsHandler{ + analyticsService: analyticsService, + transformer: transformer, + } +} + +func (h *AnalyticsHandler) GetPaymentMethodAnalytics(c *gin.Context) { + ctx := c.Request.Context() + contextInfo := appcontext.FromGinContext(ctx) + + var req contract.PaymentMethodAnalyticsRequest + if err := c.ShouldBindQuery(&req); err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("invalid_request", "AnalyticsHandler::GetPaymentMethodAnalytics", err.Error())}), "AnalyticsHandler::GetPaymentMethodAnalytics") + return + } + + req.OrganizationID = contextInfo.OrganizationID + modelReq := transformer.PaymentMethodAnalyticsContractToModel(&req) + + response, err := h.analyticsService.GetPaymentMethodAnalytics(ctx, modelReq) + if err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("internal_error", "AnalyticsHandler::GetPaymentMethodAnalytics", err.Error())}), "AnalyticsHandler::GetPaymentMethodAnalytics") + return + } + + // Transform model to contract + contractResp := transformer.PaymentMethodAnalyticsModelToContract(response) + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(contractResp), "AnalyticsHandler::GetPaymentMethodAnalytics") +} + +// GetSalesAnalytics handles the request to get sales analytics +func (h *AnalyticsHandler) GetSalesAnalytics(c *gin.Context) { + ctx := c.Request.Context() + contextInfo := appcontext.FromGinContext(ctx) + + var req contract.SalesAnalyticsRequest + if err := c.ShouldBindQuery(&req); err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("invalid_request", "AnalyticsHandler::GetSalesAnalytics", err.Error())}), "AnalyticsHandler::GetSalesAnalytics") + return + } + + req.OrganizationID = contextInfo.OrganizationID + modelReq := transformer.SalesAnalyticsContractToModel(&req) + + // Call service + response, err := h.analyticsService.GetSalesAnalytics(ctx, modelReq) + if err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("internal_error", "AnalyticsHandler::GetSalesAnalytics", err.Error())}), "AnalyticsHandler::GetSalesAnalytics") + return + } + + // Transform model to contract + contractResp := transformer.SalesAnalyticsModelToContract(response) + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(contractResp), "AnalyticsHandler::GetSalesAnalytics") +} + +// GetProductAnalytics handles the request to get product analytics +func (h *AnalyticsHandler) GetProductAnalytics(c *gin.Context) { + ctx := c.Request.Context() + contextInfo := appcontext.FromGinContext(ctx) + + var req contract.ProductAnalyticsRequest + if err := c.ShouldBindQuery(&req); err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("invalid_request", "AnalyticsHandler::GetProductAnalytics", err.Error())}), "AnalyticsHandler::GetProductAnalytics") + return + } + + req.OrganizationID = contextInfo.OrganizationID + // Transform contract to model + modelReq := transformer.ProductAnalyticsContractToModel(&req) + + // Call service + response, err := h.analyticsService.GetProductAnalytics(ctx, modelReq) + if err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("internal_error", "AnalyticsHandler::GetProductAnalytics", err.Error())}), "AnalyticsHandler::GetProductAnalytics") + return + } + + // Transform model to contract + contractResp := transformer.ProductAnalyticsModelToContract(response) + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(contractResp), "AnalyticsHandler::GetProductAnalytics") +} + +// GetDashboardAnalytics handles the request to get dashboard analytics +func (h *AnalyticsHandler) GetDashboardAnalytics(c *gin.Context) { + ctx := c.Request.Context() + contextInfo := appcontext.FromGinContext(ctx) + + var req contract.DashboardAnalyticsRequest + if err := c.ShouldBindQuery(&req); err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("invalid_request", "AnalyticsHandler::GetDashboardAnalytics", err.Error())}), "AnalyticsHandler::GetDashboardAnalytics") + return + } + + req.OrganizationID = contextInfo.OrganizationID + modelReq := transformer.DashboardAnalyticsContractToModel(&req) + + response, err := h.analyticsService.GetDashboardAnalytics(ctx, modelReq) + if err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("internal_error", "AnalyticsHandler::GetDashboardAnalytics", err.Error())}), "AnalyticsHandler::GetDashboardAnalytics") + return + } + + // Transform model to contract + contractResp := transformer.DashboardAnalyticsModelToContract(response) + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(contractResp), "AnalyticsHandler::GetDashboardAnalytics") +} + +func (h *AnalyticsHandler) GetProfitLossAnalytics(c *gin.Context) { + ctx := c.Request.Context() + contextInfo := appcontext.FromGinContext(ctx) + + var req contract.ProfitLossAnalyticsRequest + if err := c.ShouldBindQuery(&req); err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("invalid_request", "AnalyticsHandler::GetProfitLossAnalytics", err.Error())}), "AnalyticsHandler::GetProfitLossAnalytics") + return + } + + req.OrganizationID = contextInfo.OrganizationID + modelReq, err := transformer.ProfitLossAnalyticsContractToModel(&req) + if err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("invalid_request", "AnalyticsHandler::GetProfitLossAnalytics", err.Error())}), "AnalyticsHandler::GetProfitLossAnalytics") + return + } + + // Call service + response, err := h.analyticsService.GetProfitLossAnalytics(ctx, modelReq) + if err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("internal_error", "AnalyticsHandler::GetProfitLossAnalytics", err.Error())}), "AnalyticsHandler::GetProfitLossAnalytics") + return + } + + // Transform model to contract + contractResp := transformer.ProfitLossAnalyticsModelToContract(response) + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(contractResp), "AnalyticsHandler::GetProfitLossAnalytics") +} diff --git a/internal/handler/auth_handler.go b/internal/handler/auth_handler.go new file mode 100644 index 0000000..53a128a --- /dev/null +++ b/internal/handler/auth_handler.go @@ -0,0 +1,169 @@ +package handler + +import ( + "net/http" + "strings" + + "apskel-pos-be/internal/constants" + "apskel-pos-be/internal/contract" + "apskel-pos-be/internal/logger" + "apskel-pos-be/internal/service" + "apskel-pos-be/internal/transformer" + + "github.com/gin-gonic/gin" +) + +type AuthHandler struct { + authService service.AuthService +} + +func NewAuthHandler(authService service.AuthService) *AuthHandler { + return &AuthHandler{ + authService: authService, + } +} + +func (h *AuthHandler) Login(c *gin.Context) { + var req contract.LoginRequest + if err := c.ShouldBindJSON(&req); err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("AuthHandler::Login -> request binding failed") + h.sendValidationErrorResponse(c, "Invalid request body", constants.MissingFieldErrorCode) + return + } + + if strings.TrimSpace(req.Email) == "" { + logger.FromContext(c.Request.Context()).Error("AuthHandler::Login -> email is required") + h.sendValidationErrorResponse(c, "Email is required", constants.MissingFieldErrorCode) + return + } + + if strings.TrimSpace(req.Password) == "" { + logger.FromContext(c.Request.Context()).Error("AuthHandler::Login -> password is required") + h.sendValidationErrorResponse(c, "Password is required", constants.MissingFieldErrorCode) + return + } + + loginResponse, err := h.authService.Login(c.Request.Context(), &req) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("AuthHandler::Login -> Failed to login") + h.sendErrorResponse(c, err.Error(), http.StatusUnauthorized) + return + } + + logger.FromContext(c.Request.Context()).Infof("AuthHandler::Login -> Successfully logged in user = %s", loginResponse.User.Email) + c.JSON(http.StatusOK, loginResponse) +} + +func (h *AuthHandler) Logout(c *gin.Context) { + token := h.extractTokenFromHeader(c) + if token == "" { + logger.FromContext(c.Request.Context()).Error("AuthHandler::Logout -> token is required") + h.sendErrorResponse(c, "Token is required", http.StatusUnauthorized) + return + } + + err := h.authService.Logout(c.Request.Context(), token) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("AuthHandler::Logout -> Failed to logout") + h.sendErrorResponse(c, err.Error(), http.StatusUnauthorized) + return + } + + logger.FromContext(c.Request.Context()).Info("AuthHandler::Logout -> Successfully logged out") + c.JSON(http.StatusOK, transformer.CreateSuccessResponse("Successfully logged out", nil)) +} + +func (h *AuthHandler) RefreshToken(c *gin.Context) { + token := h.extractTokenFromHeader(c) + if token == "" { + logger.FromContext(c.Request.Context()).Error("AuthHandler::RefreshToken -> token is required") + h.sendErrorResponse(c, "Token is required", http.StatusUnauthorized) + return + } + + loginResponse, err := h.authService.RefreshToken(c.Request.Context(), token) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("AuthHandler::RefreshToken -> Failed to refresh token") + h.sendErrorResponse(c, err.Error(), http.StatusUnauthorized) + return + } + + logger.FromContext(c.Request.Context()).Infof("AuthHandler::RefreshToken -> Successfully refreshed token for user = %s", loginResponse.User.Email) + c.JSON(http.StatusOK, loginResponse) +} + +func (h *AuthHandler) ValidateToken(c *gin.Context) { + token := h.extractTokenFromHeader(c) + if token == "" { + logger.FromContext(c.Request.Context()).Error("AuthHandler::ValidateToken -> token is required") + h.sendErrorResponse(c, "Token is required", http.StatusUnauthorized) + return + } + + userResponse, err := h.authService.ValidateToken(token) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("AuthHandler::ValidateToken -> Failed to validate token") + h.sendErrorResponse(c, err.Error(), http.StatusUnauthorized) + return + } + + logger.FromContext(c.Request.Context()).Infof("AuthHandler::ValidateToken -> Successfully validated token for user = %s", userResponse.Email) + c.JSON(http.StatusOK, userResponse) +} + +func (h *AuthHandler) GetProfile(c *gin.Context) { + token := h.extractTokenFromHeader(c) + if token == "" { + logger.FromContext(c.Request.Context()).Error("AuthHandler::GetProfile -> token is required") + h.sendErrorResponse(c, "Token is required", http.StatusUnauthorized) + return + } + + userResponse, err := h.authService.ValidateToken(token) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("AuthHandler::GetProfile -> Failed to get profile") + h.sendErrorResponse(c, err.Error(), http.StatusUnauthorized) + return + } + + logger.FromContext(c.Request.Context()).Infof("AuthHandler::GetProfile -> Successfully retrieved profile for user = %s", userResponse.Email) + c.JSON(http.StatusOK, userResponse) +} + +// Helper methods +func (h *AuthHandler) extractTokenFromHeader(c *gin.Context) string { + authHeader := c.GetHeader("Authorization") + if authHeader == "" { + return "" + } + + // Expected format: "Bearer " + parts := strings.Split(authHeader, " ") + if len(parts) != 2 || parts[0] != "Bearer" { + return "" + } + + return parts[1] +} + +func (h *AuthHandler) sendErrorResponse(c *gin.Context, message string, statusCode int) { + errorResponse := &contract.ErrorResponse{ + Error: "error", + Message: message, + Code: statusCode, + } + c.JSON(statusCode, errorResponse) +} + +func (h *AuthHandler) sendValidationErrorResponse(c *gin.Context, message string, errorCode string) { + errorResponse := &contract.ErrorResponse{ + Error: "validation_error", + Message: message, + Code: http.StatusBadRequest, + Details: map[string]interface{}{ + "error_code": errorCode, + "entity": constants.AuthHandlerEntity, + }, + } + c.JSON(http.StatusBadRequest, errorResponse) +} diff --git a/internal/handler/category_handler.go b/internal/handler/category_handler.go new file mode 100644 index 0000000..6f7d87e --- /dev/null +++ b/internal/handler/category_handler.go @@ -0,0 +1,189 @@ +package handler + +import ( + "strconv" + + "apskel-pos-be/internal/appcontext" + "apskel-pos-be/internal/constants" + "apskel-pos-be/internal/contract" + "apskel-pos-be/internal/logger" + "apskel-pos-be/internal/service" + "apskel-pos-be/internal/util" + "apskel-pos-be/internal/validator" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +type CategoryHandler struct { + categoryService service.CategoryService + categoryValidator validator.CategoryValidator +} + +func NewCategoryHandler( + categoryService service.CategoryService, + categoryValidator validator.CategoryValidator, +) *CategoryHandler { + return &CategoryHandler{ + categoryService: categoryService, + categoryValidator: categoryValidator, + } +} + +func (h *CategoryHandler) CreateCategory(c *gin.Context) { + ctx := c.Request.Context() + contextInfo := appcontext.FromGinContext(ctx) + + var req contract.CreateCategoryRequest + if err := c.ShouldBindJSON(&req); err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("CategoryHandler::CreateCategory -> request binding failed") + validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "CategoryHandler::CreateCategory") + return + } + + validationError, validationErrorCode := h.categoryValidator.ValidateCreateCategoryRequest(&req) + if validationError != nil { + validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "CategoryHandler::CreateCategory") + return + } + + categoryResponse := h.categoryService.CreateCategory(ctx, contextInfo, &req) + if categoryResponse.HasErrors() { + errorResp := categoryResponse.GetErrors()[0] + logger.FromContext(ctx).WithError(errorResp).Error("CategoryHandler::CreateCategory -> Failed to create category from service") + } + + util.HandleResponse(c.Writer, c.Request, categoryResponse, "CategoryHandler::CreateCategory") +} + +func (h *CategoryHandler) UpdateCategory(c *gin.Context) { + ctx := c.Request.Context() + + categoryIDStr := c.Param("id") + categoryID, err := uuid.Parse(categoryIDStr) + if err != nil { + logger.FromContext(ctx).WithError(err).Error("CategoryHandler::UpdateCategory -> Invalid category ID") + validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid category ID") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "CategoryHandler::UpdateCategory") + return + } + + var req contract.UpdateCategoryRequest + if err := c.ShouldBindJSON(&req); err != nil { + logger.FromContext(ctx).WithError(err).Error("CategoryHandler::UpdateCategory -> request binding failed") + validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, "Invalid request body") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "CategoryHandler::UpdateCategory") + return + } + + validationError, validationErrorCode := h.categoryValidator.ValidateUpdateCategoryRequest(&req) + if validationError != nil { + validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "CategoryHandler::UpdateCategory") + return + } + + categoryResponse := h.categoryService.UpdateCategory(ctx, categoryID, &req) + if categoryResponse.HasErrors() { + errorResp := categoryResponse.GetErrors()[0] + logger.FromContext(ctx).WithError(errorResp).Error("CategoryHandler::UpdateCategory -> Failed to update category from service") + } + + util.HandleResponse(c.Writer, c.Request, categoryResponse, "CategoryHandler::UpdateCategory") +} + +func (h *CategoryHandler) DeleteCategory(c *gin.Context) { + ctx := c.Request.Context() + + categoryIDStr := c.Param("id") + categoryID, err := uuid.Parse(categoryIDStr) + if err != nil { + logger.FromContext(ctx).WithError(err).Error("CategoryHandler::DeleteCategory -> Invalid category ID") + validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid category ID") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "CategoryHandler::DeleteCategory") + return + } + + categoryResponse := h.categoryService.DeleteCategory(ctx, categoryID) + if categoryResponse.HasErrors() { + errorResp := categoryResponse.GetErrors()[0] + logger.FromContext(ctx).WithError(errorResp).Error("CategoryHandler::DeleteCategory -> Failed to delete category from service") + } + + util.HandleResponse(c.Writer, c.Request, categoryResponse, "CategoryHandler::DeleteCategory") +} + +func (h *CategoryHandler) GetCategory(c *gin.Context) { + ctx := c.Request.Context() + + categoryIDStr := c.Param("id") + categoryID, err := uuid.Parse(categoryIDStr) + if err != nil { + logger.FromContext(ctx).WithError(err).Error("CategoryHandler::GetCategory -> Invalid category ID") + validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid category ID") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "CategoryHandler::GetCategory") + return + } + + categoryResponse := h.categoryService.GetCategoryByID(ctx, categoryID) + if categoryResponse.HasErrors() { + errorResp := categoryResponse.GetErrors()[0] + logger.FromContext(ctx).WithError(errorResp).Error("CategoryHandler::GetCategory -> Failed to get category from service") + } + + util.HandleResponse(c.Writer, c.Request, categoryResponse, "CategoryHandler::GetCategory") +} + +func (h *CategoryHandler) ListCategories(c *gin.Context) { + ctx := c.Request.Context() + + req := &contract.ListCategoriesRequest{ + Page: 1, + Limit: 10, + } + + // Parse query parameters + if pageStr := c.Query("page"); pageStr != "" { + if page, err := strconv.Atoi(pageStr); err == nil { + req.Page = page + } + } + + if limitStr := c.Query("limit"); limitStr != "" { + if limit, err := strconv.Atoi(limitStr); err == nil { + req.Limit = limit + } + } + + if search := c.Query("search"); search != "" { + req.Search = search + } + + if businessType := c.Query("business_type"); businessType != "" { + req.BusinessType = businessType + } + + if organizationIDStr := c.Query("organization_id"); organizationIDStr != "" { + if organizationID, err := uuid.Parse(organizationIDStr); err == nil { + req.OrganizationID = &organizationID + } + } + + validationError, validationErrorCode := h.categoryValidator.ValidateListCategoriesRequest(req) + if validationError != nil { + logger.FromContext(ctx).WithError(validationError).Error("CategoryHandler::ListCategories -> request validation failed") + validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "CategoryHandler::ListCategories") + return + } + + categoriesResponse := h.categoryService.ListCategories(ctx, req) + if categoriesResponse.HasErrors() { + errorResp := categoriesResponse.GetErrors()[0] + logger.FromContext(ctx).WithError(errorResp).Error("CategoryHandler::ListCategories -> Failed to list categories from service") + } + + util.HandleResponse(c.Writer, c.Request, categoriesResponse, "CategoryHandler::ListCategories") +} diff --git a/internal/handler/common.go b/internal/handler/common.go new file mode 100644 index 0000000..eb801a2 --- /dev/null +++ b/internal/handler/common.go @@ -0,0 +1,57 @@ +package handler + +import ( + "net/http" + "time" +) + +type CommonMiddleware struct{} + +func NewCommonMiddleware() *CommonMiddleware { + return &CommonMiddleware{} +} + +func (m *CommonMiddleware) CORS(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") + + if r.Method == "OPTIONS" { + w.WriteHeader(http.StatusOK) + return + } + + next.ServeHTTP(w, r) + }) +} + +func (m *CommonMiddleware) ContentType(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + next.ServeHTTP(w, r) + }) +} + +func (m *CommonMiddleware) Logging(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + + next.ServeHTTP(w, r) + + _ = time.Since(start) + }) +} + +func (m *CommonMiddleware) Recovery(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer func() { + if err := recover(); err != nil { + + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + } + }() + + next.ServeHTTP(w, r) + }) +} diff --git a/internal/handler/customer_handler.go b/internal/handler/customer_handler.go new file mode 100644 index 0000000..8f34f39 --- /dev/null +++ b/internal/handler/customer_handler.go @@ -0,0 +1,257 @@ +package handler + +import ( + "apskel-pos-be/internal/appcontext" + "apskel-pos-be/internal/constants" + "apskel-pos-be/internal/contract" + "apskel-pos-be/internal/logger" + "apskel-pos-be/internal/service" + "apskel-pos-be/internal/util" + "apskel-pos-be/internal/validator" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +type CustomerHandler struct { + customerService service.CustomerService + customerValidator validator.CustomerValidator +} + +func NewCustomerHandler( + customerService service.CustomerService, + customerValidator validator.CustomerValidator, +) *CustomerHandler { + return &CustomerHandler{ + customerService: customerService, + customerValidator: customerValidator, + } +} + +func (h *CustomerHandler) CreateCustomer(c *gin.Context) { + ctx := c.Request.Context() + contextInfo := appcontext.FromGinContext(ctx) + + var req contract.CreateCustomerRequest + if err := c.ShouldBindJSON(&req); err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("CustomerHandler::CreateCustomer -> request binding failed") + validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "CustomerHandler::CreateCustomer") + return + } + + validationError, validationErrorCode := h.customerValidator.ValidateCreateCustomerRequest(&req) + if validationError != nil { + logger.FromContext(c.Request.Context()).WithError(validationError).Error("CustomerHandler::CreateCustomer -> request validation failed") + validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "CustomerHandler::CreateCustomer") + return + } + + organizationID := contextInfo.OrganizationID + customerResponse, err := h.customerService.CreateCustomer(c.Request.Context(), &req, organizationID) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("CustomerHandler::CreateCustomer -> Failed to create customer from service") + errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.CustomerServiceEntity, err.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{errorResp}), "CustomerHandler::CreateCustomer") + return + } + + logger.FromContext(c.Request.Context()).Infof("CustomerHandler::CreateCustomer -> Successfully created customer = %+v", customerResponse) + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(customerResponse), "CustomerHandler::CreateCustomer") +} + +func (h *CustomerHandler) GetCustomer(c *gin.Context) { + ctx := c.Request.Context() + contextInfo := appcontext.FromGinContext(ctx) + + customerID, err := uuid.Parse(c.Param("id")) + if err != nil { + logger.FromContext(ctx).WithError(err).Error("CustomerHandler::GetCustomer -> Invalid customer ID") + validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid customer ID") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "CustomerHandler::GetCustomer") + return + } + + organizationID := contextInfo.OrganizationID + customerResponse, err := h.customerService.GetCustomer(c.Request.Context(), customerID, organizationID) + if err != nil { + logger.FromContext(ctx).WithError(err).Error("CustomerHandler::GetCustomer -> Failed to get customer from service") + errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.CustomerServiceEntity, err.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{errorResp}), "CustomerHandler::GetCustomer") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(customerResponse), "CustomerHandler::GetCustomer") +} + +func (h *CustomerHandler) ListCustomers(c *gin.Context) { + ctx := c.Request.Context() + contextInfo := appcontext.FromGinContext(ctx) + + req := &contract.ListCustomersRequest{ + Page: 1, + Limit: 10, + } + + // Parse query parameters + if pageStr := c.Query("page"); pageStr != "" { + if page, err := strconv.Atoi(pageStr); err == nil { + req.Page = page + } + } + + if limitStr := c.Query("limit"); limitStr != "" { + if limit, err := strconv.Atoi(limitStr); err == nil { + req.Limit = limit + } + } + + req.Search = c.Query("search") + req.SortBy = c.Query("sort_by") + req.SortOrder = c.Query("sort_order") + + if isActiveStr := c.Query("is_active"); isActiveStr != "" { + if isActive, err := strconv.ParseBool(isActiveStr); err == nil { + req.IsActive = &isActive + } + } + + if isDefaultStr := c.Query("is_default"); isDefaultStr != "" { + if isDefault, err := strconv.ParseBool(isDefaultStr); err == nil { + req.IsDefault = &isDefault + } + } + + validationError, validationErrorCode := h.customerValidator.ValidateListCustomersRequest(req) + if validationError != nil { + logger.FromContext(ctx).WithError(validationError).Error("CustomerHandler::ListCustomers -> request validation failed") + validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "CustomerHandler::ListCustomers") + return + } + + organizationID := contextInfo.OrganizationID + customersResponse, err := h.customerService.ListCustomers(c.Request.Context(), req, organizationID) + if err != nil { + logger.FromContext(ctx).WithError(err).Error("CustomerHandler::ListCustomers -> Failed to list customers from service") + errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.CustomerServiceEntity, err.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{errorResp}), "CustomerHandler::ListCustomers") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(customersResponse), "CustomerHandler::ListCustomers") +} + +func (h *CustomerHandler) UpdateCustomer(c *gin.Context) { + ctx := c.Request.Context() + contextInfo := appcontext.FromGinContext(ctx) + + customerID, err := uuid.Parse(c.Param("id")) + if err != nil { + logger.FromContext(ctx).WithError(err).Error("CustomerHandler::UpdateCustomer -> Invalid customer ID") + validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid customer ID") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "CustomerHandler::UpdateCustomer") + return + } + + var req contract.UpdateCustomerRequest + if err := c.ShouldBindJSON(&req); err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("CustomerHandler::UpdateCustomer -> request binding failed") + validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "CustomerHandler::UpdateCustomer") + return + } + + validationError, validationErrorCode := h.customerValidator.ValidateUpdateCustomerRequest(&req) + if validationError != nil { + logger.FromContext(c.Request.Context()).WithError(validationError).Error("CustomerHandler::UpdateCustomer -> request validation failed") + validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "CustomerHandler::UpdateCustomer") + return + } + + organizationID := contextInfo.OrganizationID + customerResponse, err := h.customerService.UpdateCustomer(c.Request.Context(), customerID, organizationID, &req) + if err != nil { + logger.FromContext(ctx).WithError(err).Error("CustomerHandler::UpdateCustomer -> Failed to update customer from service") + errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.CustomerServiceEntity, err.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{errorResp}), "CustomerHandler::UpdateCustomer") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(customerResponse), "CustomerHandler::UpdateCustomer") +} + +func (h *CustomerHandler) DeleteCustomer(c *gin.Context) { + ctx := c.Request.Context() + contextInfo := appcontext.FromGinContext(ctx) + + customerID, err := uuid.Parse(c.Param("id")) + if err != nil { + logger.FromContext(ctx).WithError(err).Error("CustomerHandler::DeleteCustomer -> Invalid customer ID") + validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid customer ID") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "CustomerHandler::DeleteCustomer") + return + } + + organizationID := contextInfo.OrganizationID + err = h.customerService.DeleteCustomer(c.Request.Context(), customerID, organizationID) + if err != nil { + logger.FromContext(ctx).WithError(err).Error("CustomerHandler::DeleteCustomer -> Failed to delete customer from service") + errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.CustomerServiceEntity, err.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{errorResp}), "CustomerHandler::DeleteCustomer") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(gin.H{"message": "Customer deleted successfully"}), "CustomerHandler::DeleteCustomer") +} + +func (h *CustomerHandler) SetDefaultCustomer(c *gin.Context) { + ctx := c.Request.Context() + contextInfo := appcontext.FromGinContext(ctx) + + var req contract.SetDefaultCustomerRequest + if err := c.ShouldBindJSON(&req); err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("CustomerHandler::SetDefaultCustomer -> request binding failed") + validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "CustomerHandler::SetDefaultCustomer") + return + } + + validationError, validationErrorCode := h.customerValidator.ValidateSetDefaultCustomerRequest(&req) + if validationError != nil { + logger.FromContext(c.Request.Context()).WithError(validationError).Error("CustomerHandler::SetDefaultCustomer -> request validation failed") + validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "CustomerHandler::SetDefaultCustomer") + return + } + + organizationID := contextInfo.OrganizationID + customerResponse, err := h.customerService.SetDefaultCustomer(c.Request.Context(), req.CustomerID, organizationID) + if err != nil { + logger.FromContext(ctx).WithError(err).Error("CustomerHandler::SetDefaultCustomer -> Failed to set default customer from service") + errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.CustomerServiceEntity, err.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{errorResp}), "CustomerHandler::SetDefaultCustomer") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(customerResponse), "CustomerHandler::SetDefaultCustomer") +} + +func (h *CustomerHandler) GetDefaultCustomer(c *gin.Context) { + ctx := c.Request.Context() + contextInfo := appcontext.FromGinContext(ctx) + + organizationID := contextInfo.OrganizationID + customerResponse, err := h.customerService.GetDefaultCustomer(c.Request.Context(), organizationID) + if err != nil { + logger.FromContext(ctx).WithError(err).Error("CustomerHandler::GetDefaultCustomer -> Failed to get default customer from service") + errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.CustomerServiceEntity, err.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{errorResp}), "CustomerHandler::GetDefaultCustomer") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(customerResponse), "CustomerHandler::GetDefaultCustomer") +} diff --git a/internal/handler/file_handler.go b/internal/handler/file_handler.go new file mode 100644 index 0000000..f3ee154 --- /dev/null +++ b/internal/handler/file_handler.go @@ -0,0 +1,181 @@ +package handler + +import ( + "apskel-pos-be/internal/appcontext" + "apskel-pos-be/internal/contract" + "apskel-pos-be/internal/service" + "apskel-pos-be/internal/transformer" + "apskel-pos-be/internal/validator" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +type FileHandler struct { + fileService service.FileService + validator validator.FileValidator + transformer transformer.Transformer +} + +func NewFileHandler( + fileService service.FileService, + validator validator.FileValidator, + transformer transformer.Transformer, +) *FileHandler { + return &FileHandler{ + fileService: fileService, + validator: validator, + transformer: transformer, + } +} + +func (h *FileHandler) UploadFile(c *gin.Context) { + ctx := c.Request.Context() + contextInfo := appcontext.FromGinContext(ctx) + + file, err := c.FormFile("file") + if err != nil { + h.transformer.Error(c, http.StatusBadRequest, "File is required", err) + return + } + + var req contract.UploadFileRequest + if fileType := c.PostForm("file_type"); fileType != "" { + req.FileType = fileType + } + + if isPublicStr := c.PostForm("is_public"); isPublicStr != "" { + if isPublic, err := strconv.ParseBool(isPublicStr); err == nil { + req.IsPublic = &isPublic + } + } + + if err := h.validator.Validate(&req); err != nil { + h.transformer.Error(c, http.StatusBadRequest, "Validation failed", err) + return + } + + organizationID := contextInfo.OrganizationID + userID := contextInfo.UserID + + // Transform contract to model + modelReq := transformer.UploadFileContractToModel(&req) + response, err := h.fileService.UploadFile(ctx, file, modelReq, organizationID, userID) + if err != nil { + h.transformer.Error(c, http.StatusInternalServerError, "Failed to upload file", err) + return + } + + // Transform model to contract + contractResp := transformer.FileModelToContract(response) + h.transformer.Success(c, http.StatusCreated, "File uploaded successfully", contractResp) +} + +func (h *FileHandler) GetFileByID(c *gin.Context) { + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + h.transformer.Error(c, http.StatusBadRequest, "Invalid file ID", err) + return + } + + response, err := h.fileService.GetFileByID(c.Request.Context(), id) + if err != nil { + h.transformer.Error(c, http.StatusInternalServerError, "Failed to get file", err) + return + } + + // Transform model to contract + contractResp := transformer.FileModelToContract(response) + h.transformer.Success(c, http.StatusOK, "File retrieved successfully", contractResp) +} + +func (h *FileHandler) UpdateFile(c *gin.Context) { + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + h.transformer.Error(c, http.StatusBadRequest, "Invalid file ID", err) + return + } + + var req contract.UpdateFileRequest + if err := c.ShouldBindJSON(&req); err != nil { + h.transformer.Error(c, http.StatusBadRequest, "Invalid request body", err) + return + } + + if err := h.validator.Validate(&req); err != nil { + h.transformer.Error(c, http.StatusBadRequest, "Validation failed", err) + return + } + + // Transform contract to model + modelReq := transformer.UpdateFileContractToModel(&req) + response, err := h.fileService.UpdateFile(c.Request.Context(), id, modelReq) + if err != nil { + h.transformer.Error(c, http.StatusInternalServerError, "Failed to update file", err) + return + } + + // Transform model to contract + contractResp := transformer.FileModelToContract(response) + h.transformer.Success(c, http.StatusOK, "File updated successfully", contractResp) +} + +func (h *FileHandler) GetFilesByOrganization(c *gin.Context) { + ctx := c.Request.Context() + contextInfo := appcontext.FromGinContext(ctx) + organizationID := contextInfo.OrganizationID + + response, err := h.fileService.GetFileByOrganizationID(ctx, organizationID) + if err != nil { + h.transformer.Error(c, http.StatusInternalServerError, "Failed to get files by organization", err) + return + } + + // Transform model to contract + contractFiles := make([]*contract.FileResponse, len(response)) + for i, file := range response { + contractFiles[i] = transformer.FileModelToContract(file) + } + + contractResp := &contract.ListFilesResponse{ + Files: contractFiles, + TotalCount: len(response), + Page: 1, + Limit: len(response), + TotalPages: 1, + } + + h.transformer.Success(c, http.StatusOK, "Files retrieved successfully", contractResp) +} + +func (h *FileHandler) GetFilesByUser(c *gin.Context) { + ctx := c.Request.Context() + contextInfo := appcontext.FromGinContext(ctx) + userID := contextInfo.UserID + + response, err := h.fileService.GetFileByUserID(ctx, userID) + if err != nil { + h.transformer.Error(c, http.StatusInternalServerError, "Failed to get files by user", err) + return + } + + // Transform model to contract + contractFiles := make([]*contract.FileResponse, len(response)) + for i, file := range response { + contractFiles[i] = transformer.FileModelToContract(file) + } + + contractResp := &contract.ListFilesResponse{ + Files: contractFiles, + TotalCount: len(response), + Page: 1, + Limit: len(response), + TotalPages: 1, + } + + h.transformer.Success(c, http.StatusOK, "Files retrieved successfully", contractResp) +} diff --git a/internal/handler/health.go b/internal/handler/health.go new file mode 100644 index 0000000..e56481a --- /dev/null +++ b/internal/handler/health.go @@ -0,0 +1,23 @@ +package handler + +import ( + "apskel-pos-be/internal/logger" + "net/http" + + "github.com/gin-gonic/gin" +) + +type HealthHandler struct { +} + +func NewHealthHandler() *HealthHandler { + return &HealthHandler{} +} + +func (hh *HealthHandler) HealthCheck(c *gin.Context) { + log := logger.NewContextLogger(c, "healthCheck") + log.Info("Health Check success") + c.JSON(http.StatusOK, gin.H{ + "status": "Healthy!!", + }) +} diff --git a/internal/handler/inventory_handler.go b/internal/handler/inventory_handler.go new file mode 100644 index 0000000..dcc28e6 --- /dev/null +++ b/internal/handler/inventory_handler.go @@ -0,0 +1,278 @@ +package handler + +import ( + "strconv" + + "apskel-pos-be/internal/appcontext" + "apskel-pos-be/internal/constants" + "apskel-pos-be/internal/contract" + "apskel-pos-be/internal/logger" + "apskel-pos-be/internal/service" + "apskel-pos-be/internal/util" + "apskel-pos-be/internal/validator" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +type InventoryHandler struct { + inventoryService service.InventoryService + inventoryValidator validator.InventoryValidator +} + +func NewInventoryHandler( + inventoryService service.InventoryService, + inventoryValidator validator.InventoryValidator, +) *InventoryHandler { + return &InventoryHandler{ + inventoryService: inventoryService, + inventoryValidator: inventoryValidator, + } +} + +func (h *InventoryHandler) CreateInventory(c *gin.Context) { + ctx := c.Request.Context() + contextInfo := appcontext.FromGinContext(ctx) + + var req contract.CreateInventoryRequest + if err := c.ShouldBindJSON(&req); err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("InventoryHandler::CreateInventory -> request binding failed") + validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "InventoryHandler::CreateInventory") + return + } + + validationError, validationErrorCode := h.inventoryValidator.ValidateCreateInventoryRequest(&req) + if validationError != nil { + validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "InventoryHandler::CreateInventory") + return + } + + inventoryResponse := h.inventoryService.CreateInventory(ctx, contextInfo, &req) + if inventoryResponse.HasErrors() { + errorResp := inventoryResponse.GetErrors()[0] + logger.FromContext(ctx).WithError(errorResp).Error("InventoryHandler::CreateInventory -> Failed to create inventory from service") + } + + util.HandleResponse(c.Writer, c.Request, inventoryResponse, "InventoryHandler::CreateInventory") +} + +func (h *InventoryHandler) UpdateInventory(c *gin.Context) { + ctx := c.Request.Context() + + inventoryIDStr := c.Param("id") + inventoryID, err := uuid.Parse(inventoryIDStr) + if err != nil { + logger.FromContext(ctx).WithError(err).Error("InventoryHandler::UpdateInventory -> Invalid inventory ID") + validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid inventory ID") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "InventoryHandler::UpdateInventory") + return + } + + var req contract.UpdateInventoryRequest + if err := c.ShouldBindJSON(&req); err != nil { + logger.FromContext(ctx).WithError(err).Error("InventoryHandler::UpdateInventory -> request binding failed") + validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, "Invalid request body") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "InventoryHandler::UpdateInventory") + return + } + + validationError, validationErrorCode := h.inventoryValidator.ValidateUpdateInventoryRequest(&req) + if validationError != nil { + validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "InventoryHandler::UpdateInventory") + return + } + + inventoryResponse := h.inventoryService.UpdateInventory(ctx, inventoryID, &req) + if inventoryResponse.HasErrors() { + errorResp := inventoryResponse.GetErrors()[0] + logger.FromContext(ctx).WithError(errorResp).Error("InventoryHandler::UpdateInventory -> Failed to update inventory from service") + } + + util.HandleResponse(c.Writer, c.Request, inventoryResponse, "InventoryHandler::UpdateInventory") +} + +func (h *InventoryHandler) DeleteInventory(c *gin.Context) { + ctx := c.Request.Context() + + inventoryIDStr := c.Param("id") + inventoryID, err := uuid.Parse(inventoryIDStr) + if err != nil { + logger.FromContext(ctx).WithError(err).Error("InventoryHandler::DeleteInventory -> Invalid inventory ID") + validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid inventory ID") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "InventoryHandler::DeleteInventory") + return + } + + inventoryResponse := h.inventoryService.DeleteInventory(ctx, inventoryID) + if inventoryResponse.HasErrors() { + errorResp := inventoryResponse.GetErrors()[0] + logger.FromContext(ctx).WithError(errorResp).Error("InventoryHandler::DeleteInventory -> Failed to delete inventory from service") + } + + util.HandleResponse(c.Writer, c.Request, inventoryResponse, "InventoryHandler::DeleteInventory") +} + +func (h *InventoryHandler) GetInventory(c *gin.Context) { + ctx := c.Request.Context() + + inventoryIDStr := c.Param("id") + inventoryID, err := uuid.Parse(inventoryIDStr) + if err != nil { + logger.FromContext(ctx).WithError(err).Error("InventoryHandler::GetInventory -> Invalid inventory ID") + validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid inventory ID") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "InventoryHandler::GetInventory") + return + } + + inventoryResponse := h.inventoryService.GetInventoryByID(ctx, inventoryID) + if inventoryResponse.HasErrors() { + errorResp := inventoryResponse.GetErrors()[0] + logger.FromContext(ctx).WithError(errorResp).Error("InventoryHandler::GetInventory -> Failed to get inventory from service") + } + + util.HandleResponse(c.Writer, c.Request, inventoryResponse, "InventoryHandler::GetInventory") +} + +func (h *InventoryHandler) ListInventory(c *gin.Context) { + ctx := c.Request.Context() + + req := &contract.ListInventoryRequest{ + Page: 1, + Limit: 10, + } + + // Parse query parameters + if pageStr := c.Query("page"); pageStr != "" { + if page, err := strconv.Atoi(pageStr); err == nil { + req.Page = page + } + } + + if limitStr := c.Query("limit"); limitStr != "" { + if limit, err := strconv.Atoi(limitStr); err == nil { + req.Limit = limit + } + } + + if search := c.Query("search"); search != "" { + req.Search = search + } + + if outletIDStr := c.Query("outlet_id"); outletIDStr != "" { + if outletID, err := uuid.Parse(outletIDStr); err == nil { + req.OutletID = &outletID + } + } + + if productIDStr := c.Query("product_id"); productIDStr != "" { + if productID, err := uuid.Parse(productIDStr); err == nil { + req.ProductID = &productID + } + } + + if categoryIDStr := c.Query("category_id"); categoryIDStr != "" { + if categoryID, err := uuid.Parse(categoryIDStr); err == nil { + req.CategoryID = &categoryID + } + } + + if lowStockStr := c.Query("low_stock_only"); lowStockStr != "" { + if lowStock, err := strconv.ParseBool(lowStockStr); err == nil { + req.LowStockOnly = &lowStock + } + } + + if zeroStockStr := c.Query("zero_stock_only"); zeroStockStr != "" { + if zeroStock, err := strconv.ParseBool(zeroStockStr); err == nil { + req.ZeroStockOnly = &zeroStock + } + } + + validationError, validationErrorCode := h.inventoryValidator.ValidateListInventoryRequest(req) + if validationError != nil { + logger.FromContext(ctx).WithError(validationError).Error("InventoryHandler::ListInventory -> request validation failed") + validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "InventoryHandler::ListInventory") + return + } + + inventoryResponse := h.inventoryService.ListInventory(ctx, req) + if inventoryResponse.HasErrors() { + errorResp := inventoryResponse.GetErrors()[0] + logger.FromContext(ctx).WithError(errorResp).Error("InventoryHandler::ListInventory -> Failed to list inventory from service") + } + + util.HandleResponse(c.Writer, c.Request, inventoryResponse, "InventoryHandler::ListInventory") +} + +func (h *InventoryHandler) AdjustInventory(c *gin.Context) { + ctx := c.Request.Context() + + var req contract.AdjustInventoryRequest + if err := c.ShouldBindJSON(&req); err != nil { + logger.FromContext(ctx).WithError(err).Error("InventoryHandler::AdjustInventory -> request binding failed") + validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "InventoryHandler::AdjustInventory") + return + } + + validationError, validationErrorCode := h.inventoryValidator.ValidateAdjustInventoryRequest(&req) + if validationError != nil { + validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "InventoryHandler::AdjustInventory") + return + } + + inventoryResponse := h.inventoryService.AdjustInventory(ctx, &req) + if inventoryResponse.HasErrors() { + errorResp := inventoryResponse.GetErrors()[0] + logger.FromContext(ctx).WithError(errorResp).Error("InventoryHandler::AdjustInventory -> Failed to adjust inventory from service") + } + + util.HandleResponse(c.Writer, c.Request, inventoryResponse, "InventoryHandler::AdjustInventory") +} + +func (h *InventoryHandler) GetLowStockItems(c *gin.Context) { + ctx := c.Request.Context() + + outletIDStr := c.Param("outlet_id") + outletID, err := uuid.Parse(outletIDStr) + if err != nil { + logger.FromContext(ctx).WithError(err).Error("InventoryHandler::GetLowStockItems -> Invalid outlet ID") + validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid outlet ID") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "InventoryHandler::GetLowStockItems") + return + } + + inventoryResponse := h.inventoryService.GetLowStockItems(ctx, outletID) + if inventoryResponse.HasErrors() { + errorResp := inventoryResponse.GetErrors()[0] + logger.FromContext(ctx).WithError(errorResp).Error("InventoryHandler::GetLowStockItems -> Failed to get low stock items from service") + } + + util.HandleResponse(c.Writer, c.Request, inventoryResponse, "InventoryHandler::GetLowStockItems") +} + +func (h *InventoryHandler) GetZeroStockItems(c *gin.Context) { + ctx := c.Request.Context() + + outletIDStr := c.Param("outlet_id") + outletID, err := uuid.Parse(outletIDStr) + if err != nil { + logger.FromContext(ctx).WithError(err).Error("InventoryHandler::GetZeroStockItems -> Invalid outlet ID") + validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid outlet ID") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "InventoryHandler::GetZeroStockItems") + return + } + + inventoryResponse := h.inventoryService.GetZeroStockItems(ctx, outletID) + if inventoryResponse.HasErrors() { + errorResp := inventoryResponse.GetErrors()[0] + logger.FromContext(ctx).WithError(errorResp).Error("InventoryHandler::GetZeroStockItems -> Failed to get zero stock items from service") + } + + util.HandleResponse(c.Writer, c.Request, inventoryResponse, "InventoryHandler::GetZeroStockItems") +} diff --git a/internal/handler/order_handler.go b/internal/handler/order_handler.go new file mode 100644 index 0000000..8ff8555 --- /dev/null +++ b/internal/handler/order_handler.go @@ -0,0 +1,296 @@ +package handler + +import ( + "apskel-pos-be/internal/appcontext" + "apskel-pos-be/internal/contract" + "apskel-pos-be/internal/service" + "apskel-pos-be/internal/transformer" + "apskel-pos-be/internal/util" + "apskel-pos-be/internal/validator" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +type OrderHandler struct { + orderService service.OrderService + validator validator.OrderValidator + transformer transformer.Transformer +} + +func NewOrderHandler( + orderService service.OrderService, + validator validator.OrderValidator, + transformer transformer.Transformer, +) *OrderHandler { + return &OrderHandler{ + orderService: orderService, + validator: validator, + transformer: transformer, + } +} + +func (h *OrderHandler) CreateOrder(c *gin.Context) { + ctx := c.Request.Context() + contextInfo := appcontext.FromGinContext(ctx) + + var req contract.CreateOrderRequest + if err := c.ShouldBindJSON(&req); err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("invalid_request", "OrderHandler::CreateOrder", err.Error())}), "OrderHandler::CreateOrder") + return + } + + req.UserID = contextInfo.UserID + modelReq := transformer.CreateOrderContractToModel(&req) + response, err := h.orderService.CreateOrder(ctx, modelReq, contextInfo.OrganizationID) + if err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("internal_error", "OrderHandler::CreateOrder", err.Error())}), "OrderHandler::CreateOrder") + return + } + + contractResp := transformer.OrderModelToContract(response) + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(contractResp), "OrderHandler::CreateOrder") +} + +func (h *OrderHandler) GetOrderByID(c *gin.Context) { + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("invalid_id", "OrderHandler::GetOrderByID", err.Error())}), "OrderHandler::GetOrderByID") + return + } + + response, err := h.orderService.GetOrderByID(c.Request.Context(), id) + if err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("internal_error", "OrderHandler::GetOrderByID", err.Error())}), "OrderHandler::GetOrderByID") + return + } + + contractResp := transformer.OrderModelToContract(response) + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(contractResp), "OrderHandler::GetOrderByID") +} + +func (h *OrderHandler) UpdateOrder(c *gin.Context) { + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("invalid_id", "OrderHandler::UpdateOrder", err.Error())}), "OrderHandler::UpdateOrder") + return + } + + var req contract.UpdateOrderRequest + if err := c.ShouldBindJSON(&req); err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("invalid_request", "OrderHandler::UpdateOrder", err.Error())}), "OrderHandler::UpdateOrder") + return + } + + modelReq := transformer.UpdateOrderContractToModel(&req) + response, err := h.orderService.UpdateOrder(c.Request.Context(), id, modelReq) + if err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("internal_error", "OrderHandler::UpdateOrder", err.Error())}), "OrderHandler::UpdateOrder") + return + } + + contractResp := transformer.OrderModelToContract(response) + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(contractResp), "OrderHandler::UpdateOrder") +} + +func (h *OrderHandler) AddToOrder(c *gin.Context) { + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("invalid_id", "OrderHandler::AddToOrder", err.Error())}), "OrderHandler::AddToOrder") + return + } + + var req contract.AddToOrderRequest + if err := c.ShouldBindJSON(&req); err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("invalid_request", "OrderHandler::AddToOrder", err.Error())}), "OrderHandler::AddToOrder") + return + } + + modelReq := transformer.AddToOrderContractToModel(&req) + response, err := h.orderService.AddToOrder(c.Request.Context(), id, modelReq) + if err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("internal_error", "OrderHandler::AddToOrder", err.Error())}), "OrderHandler::AddToOrder") + return + } + + contractResp := transformer.AddToOrderModelToContract(response) + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(contractResp), "OrderHandler::AddToOrder") +} + +func (h *OrderHandler) ListOrders(c *gin.Context) { + var query contract.ListOrdersQuery + if err := c.ShouldBindQuery(&query); err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("invalid_query_parameters", "OrderHandler::ListOrders", err.Error())}), "OrderHandler::ListOrders") + return + } + + modelReq := transformer.ListOrdersQueryToModel(&query) + if err := h.validator.Validate(modelReq); err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("validation_failed", "OrderHandler::ListOrders", err.Error())}), "OrderHandler::ListOrders") + return + } + + response, err := h.orderService.ListOrders(c.Request.Context(), modelReq) + if err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("internal_error", "OrderHandler::ListOrders", err.Error())}), "OrderHandler::ListOrders") + return + } + + contractResp := transformer.ListOrdersModelToContract(response) + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(contractResp), "OrderHandler::ListOrders") +} + +func (h *OrderHandler) VoidOrder(c *gin.Context) { + ctx := c.Request.Context() + contextInfo := appcontext.FromGinContext(ctx) + + var req contract.VoidOrderRequest + if err := c.ShouldBindJSON(&req); err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("invalid_request", "OrderHandler::VoidOrder", err.Error())}), "OrderHandler::VoidOrder") + return + } + + userID := contextInfo.UserID + if err := h.validator.Validate(&req); err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("validation_failed", "OrderHandler::VoidOrder", err.Error())}), "OrderHandler::VoidOrder") + return + } + + modelReq := transformer.VoidOrderContractToModel(&req) + if err := h.orderService.VoidOrder(ctx, modelReq, userID); err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("internal_error", "OrderHandler::VoidOrder", err.Error())}), "OrderHandler::VoidOrder") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(map[string]string{"message": "Order voided successfully"}), "OrderHandler::VoidOrder") +} + +func (h *OrderHandler) RefundOrder(c *gin.Context) { + ctx := c.Request.Context() + contextInfo := appcontext.FromGinContext(ctx) + + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("invalid_id", "OrderHandler::RefundOrder", err.Error())}), "OrderHandler::RefundOrder") + return + } + + var req contract.RefundOrderRequest + if err := c.ShouldBindJSON(&req); err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("invalid_request", "OrderHandler::RefundOrder", err.Error())}), "OrderHandler::RefundOrder") + return + } + + userID := contextInfo.UserID + + modelReq := transformer.RefundOrderContractToModel(&req) + if err := h.validator.Validate(modelReq); err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("validation_failed", "OrderHandler::RefundOrder", err.Error())}), "OrderHandler::RefundOrder") + return + } + + if err := h.orderService.RefundOrder(ctx, id, modelReq, userID); err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("internal_error", "OrderHandler::RefundOrder", err.Error())}), "OrderHandler::RefundOrder") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(nil), "OrderHandler::RefundOrder") +} + +func (h *OrderHandler) CreatePayment(c *gin.Context) { + var req contract.CreatePaymentRequest + if err := c.ShouldBindJSON(&req); err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("invalid_request", "OrderHandler::CreatePayment", err.Error())}), "OrderHandler::CreatePayment") + return + } + + if err := h.validator.Validate(req); err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("validation_failed", "OrderHandler::CreatePayment", err.Error())}), "OrderHandler::CreatePayment") + return + } + + modelReq := transformer.CreatePaymentContractToModel(&req) + + response, err := h.orderService.CreatePayment(c.Request.Context(), modelReq) + if err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("internal_error", "OrderHandler::CreatePayment", err.Error())}), "OrderHandler::CreatePayment") + return + } + + contractResp := transformer.PaymentModelToContract(response) + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(contractResp), "OrderHandler::CreatePayment") +} + +func (h *OrderHandler) RefundPayment(c *gin.Context) { + ctx := c.Request.Context() + contextInfo := appcontext.FromGinContext(ctx) + + idStr := c.Param("id") + paymentID, err := uuid.Parse(idStr) + if err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("invalid_id", "OrderHandler::RefundPayment", err.Error())}), "OrderHandler::RefundPayment") + return + } + + var req contract.RefundPaymentRequest + if err := c.ShouldBindJSON(&req); err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("invalid_request", "OrderHandler::RefundPayment", err.Error())}), "OrderHandler::RefundPayment") + return + } + + userID := contextInfo.UserID + if userID == uuid.Nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("unauthorized", "OrderHandler::RefundPayment", "Invalid User ID in context")}), "OrderHandler::RefundPayment") + return + } + + if err := h.validator.Validate(&req); err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("validation_failed", "OrderHandler::RefundPayment", err.Error())}), "OrderHandler::RefundPayment") + return + } + + if err := h.orderService.RefundPayment(ctx, paymentID, req.RefundAmount, req.Reason, userID); err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("internal_error", "OrderHandler::RefundPayment", err.Error())}), "OrderHandler::RefundPayment") + return + } + + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(nil), "OrderHandler::RefundPayment") +} + +func (h *OrderHandler) SetOrderCustomer(c *gin.Context) { + ctx := c.Request.Context() + contextInfo := appcontext.FromGinContext(ctx) + + // Parse order ID from URL parameter + orderIDStr := c.Param("id") + orderID, err := uuid.Parse(orderIDStr) + if err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("invalid_id", "OrderHandler::SetOrderCustomer", "Invalid order ID")}), "OrderHandler::SetOrderCustomer") + return + } + + // Parse request body + var req contract.SetOrderCustomerRequest + if err := c.ShouldBindJSON(&req); err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("invalid_request", "OrderHandler::SetOrderCustomer", err.Error())}), "OrderHandler::SetOrderCustomer") + return + } + + // Transform contract to model + modelReq := transformer.SetOrderCustomerContractToModel(&req) + + // Call service + response, err := h.orderService.SetOrderCustomer(ctx, orderID, modelReq, contextInfo.OrganizationID) + if err != nil { + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{contract.NewResponseError("internal_error", "OrderHandler::SetOrderCustomer", err.Error())}), "OrderHandler::SetOrderCustomer") + return + } + + // Transform model to contract + contractResp := transformer.SetOrderCustomerModelToContract(response) + util.HandleResponse(c.Writer, c.Request, contract.BuildSuccessResponse(contractResp), "OrderHandler::SetOrderCustomer") +} diff --git a/internal/handler/organization_handler.go b/internal/handler/organization_handler.go new file mode 100644 index 0000000..53f2fd8 --- /dev/null +++ b/internal/handler/organization_handler.go @@ -0,0 +1,185 @@ +package handler + +import ( + "apskel-pos-be/internal/appcontext" + "apskel-pos-be/internal/util" + "strconv" + + "apskel-pos-be/internal/constants" + "apskel-pos-be/internal/contract" + "apskel-pos-be/internal/logger" + "apskel-pos-be/internal/service" + "apskel-pos-be/internal/validator" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +type OrganizationHandler struct { + organizationService service.OrganizationService + organizationValidator validator.OrganizationValidator +} + +func NewOrganizationHandler( + organizationService service.OrganizationService, + organizationValidator validator.OrganizationValidator, +) *OrganizationHandler { + return &OrganizationHandler{ + organizationService: organizationService, + organizationValidator: organizationValidator, + } +} + +func (h *OrganizationHandler) CreateOrganization(c *gin.Context) { + ctx := c.Request.Context() + contextInfo := appcontext.FromGinContext(ctx) + + var req contract.CreateOrganizationRequest + if err := c.ShouldBindJSON(&req); err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("OrganizationHandler::CreateOrganization -> request binding failed") + validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "OrganizationHandler::CreateOrganization") + return + } + + validationError, validationErrorCode := h.organizationValidator.ValidateCreateOrganizationRequest(&req) + if validationError != nil { + validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "OrganizationHandler::CreateOrganization") + return + } + + organizationResponse := h.organizationService.CreateOrganization(ctx, contextInfo, &req) + if organizationResponse.HasErrors() { + errorResp := organizationResponse.GetErrors()[0] + logger.FromContext(ctx).WithError(errorResp).Error("OrganizationHandler::CreateOrganization -> Failed to create organization from service") + } + + util.HandleResponse(c.Writer, c.Request, organizationResponse, "OrganizationHandler::CreateOrganization") +} + +func (h *OrganizationHandler) UpdateOrganization(c *gin.Context) { + ctx := c.Request.Context() + + organizationIDStr := c.Param("id") + organizationID, err := uuid.Parse(organizationIDStr) + if err != nil { + logger.FromContext(ctx).WithError(err).Error("OrganizationHandler::UpdateOrganization -> Invalid organization ID") + validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid organization ID") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "OrganizationHandler::UpdateOrganization") + return + } + + var req contract.UpdateOrganizationRequest + if err := c.ShouldBindJSON(&req); err != nil { + logger.FromContext(ctx).WithError(err).Error("OrganizationHandler::UpdateOrganization -> request binding failed") + validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, "Invalid request body") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "OrganizationHandler::UpdateOrganization") + return + } + + validationError, validationErrorCode := h.organizationValidator.ValidateUpdateOrganizationRequest(&req) + if validationError != nil { + validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "OrganizationHandler::UpdateOrganization") + return + } + + organizationResponse := h.organizationService.UpdateOrganization(ctx, organizationID, &req) + if organizationResponse.HasErrors() { + errorResp := organizationResponse.GetErrors()[0] + logger.FromContext(ctx).WithError(errorResp).Error("OrganizationHandler::UpdateOrganization -> Failed to update organization from service") + } + + util.HandleResponse(c.Writer, c.Request, organizationResponse, "OrganizationHandler::UpdateOrganization") +} + +func (h *OrganizationHandler) DeleteOrganization(c *gin.Context) { + ctx := c.Request.Context() + + organizationIDStr := c.Param("id") + organizationID, err := uuid.Parse(organizationIDStr) + if err != nil { + logger.FromContext(ctx).WithError(err).Error("OrganizationHandler::DeleteOrganization -> Invalid organization ID") + validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid organization ID") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "OrganizationHandler::DeleteOrganization") + return + } + + organizationResponse := h.organizationService.DeleteOrganization(ctx, organizationID) + if organizationResponse.HasErrors() { + errorResp := organizationResponse.GetErrors()[0] + logger.FromContext(ctx).WithError(errorResp).Error("OrganizationHandler::DeleteOrganization -> Failed to delete organization from service") + } + + util.HandleResponse(c.Writer, c.Request, organizationResponse, "OrganizationHandler::DeleteOrganization") +} + +func (h *OrganizationHandler) GetOrganization(c *gin.Context) { + ctx := c.Request.Context() + + organizationIDStr := c.Param("id") + organizationID, err := uuid.Parse(organizationIDStr) + if err != nil { + logger.FromContext(ctx).WithError(err).Error("OrganizationHandler::GetOrganization -> Invalid organization ID") + validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid organization ID") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "OrganizationHandler::GetOrganization") + return + } + + organizationResponse := h.organizationService.GetOrganizationByID(ctx, organizationID) + if organizationResponse.HasErrors() { + errorResp := organizationResponse.GetErrors()[0] + logger.FromContext(ctx).WithError(errorResp).Error("OrganizationHandler::GetOrganization -> Failed to get organization from service") + } + + util.HandleResponse(c.Writer, c.Request, organizationResponse, "OrganizationHandler::GetOrganization") +} + +func (h *OrganizationHandler) ListOrganizations(c *gin.Context) { + ctx := c.Request.Context() + + req := &contract.ListOrganizationsRequest{ + Page: 1, + Limit: 10, + } + + // Parse query parameters + if pageStr := c.Query("page"); pageStr != "" { + if page, err := strconv.Atoi(pageStr); err == nil { + req.Page = page + } + } + + if limitStr := c.Query("limit"); limitStr != "" { + if limit, err := strconv.Atoi(limitStr); err == nil { + req.Limit = limit + } + } + + if search := c.Query("search"); search != "" { + req.Search = search + } + + if planType := c.Query("plan_type"); planType != "" { + req.PlanType = planType + } + + validationError, validationErrorCode := h.organizationValidator.ValidateListOrganizationsRequest(req) + if validationError != nil { + logger.FromContext(ctx).WithError(validationError).Error("OrganizationHandler::ListOrganizations -> request validation failed") + validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "OrganizationHandler::ListOrganizations") + return + } + + organizationsResponse := h.organizationService.ListOrganizations(ctx, req) + if organizationsResponse.HasErrors() { + errorResp := organizationsResponse.GetErrors()[0] + logger.FromContext(ctx).WithError(errorResp).Error("OrganizationHandler::ListOrganizations -> Failed to list organizations from service") + } + + util.HandleResponse(c.Writer, c.Request, organizationsResponse, "OrganizationHandler::ListOrganizations") +} + +// Old error response methods removed - now using util.HandleResponse pattern diff --git a/internal/handler/outlet_handler.go b/internal/handler/outlet_handler.go new file mode 100644 index 0000000..2c995d6 --- /dev/null +++ b/internal/handler/outlet_handler.go @@ -0,0 +1,144 @@ +package handler + +import ( + "apskel-pos-be/internal/appcontext" + "apskel-pos-be/internal/constants" + "apskel-pos-be/internal/contract" + "apskel-pos-be/internal/logger" + "apskel-pos-be/internal/service" + "apskel-pos-be/internal/util" + "apskel-pos-be/internal/validator" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +type OutletHandler struct { + outletService service.OutletService + outletValidator validator.OutletValidator +} + +func NewOutletHandler(outletService service.OutletService, outletValidator validator.OutletValidator) *OutletHandler { + return &OutletHandler{ + outletService: outletService, + outletValidator: outletValidator, + } +} + +func (h *OutletHandler) ListOutlets(c *gin.Context) { + ctx := c.Request.Context() + contextInfo := appcontext.FromGinContext(ctx) + + req := &contract.ListOutletsRequest{ + Page: 1, + Limit: 10, + OrganizationID: contextInfo.OrganizationID, + } + + // Parse query parameters + if pageStr := c.Query("page"); pageStr != "" { + if page, err := strconv.Atoi(pageStr); err == nil { + req.Page = page + } + } + + if limitStr := c.Query("limit"); limitStr != "" { + if limit, err := strconv.Atoi(limitStr); err == nil { + req.Limit = limit + } + } + + if search := c.Query("search"); search != "" { + req.Search = search + } + + if businessType := c.Query("business_type"); businessType != "" { + req.BusinessType = &businessType + } + + if isActiveStr := c.Query("is_active"); isActiveStr != "" { + if isActive, err := strconv.ParseBool(isActiveStr); err == nil { + req.IsActive = &isActive + } + } + + validationError, validationErrorCode := h.outletValidator.ValidateListOutletsRequest(req) + if validationError != nil { + logger.FromContext(ctx).WithError(validationError).Error("OutletHandler::ListOutlets -> request validation failed") + validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "OutletHandler::ListOutlets") + return + } + + outletsResponse := h.outletService.ListOutlets(ctx, req) + if outletsResponse.HasErrors() { + errorResp := outletsResponse.GetErrors()[0] + logger.FromContext(ctx).WithError(errorResp).Error("OutletHandler::ListOutlets -> Failed to list outlets from service") + } + + util.HandleResponse(c.Writer, c.Request, outletsResponse, "OutletHandler::ListOutlets") +} + +func (h *OutletHandler) GetOutlet(c *gin.Context) { + ctx := c.Request.Context() + contextInfo := appcontext.FromGinContext(ctx) + + outletIDStr := c.Param("id") + outletID, err := uuid.Parse(outletIDStr) + + if err != nil { + logger.FromContext(ctx).WithError(err).Error("OutletHandler::GetOutlet -> Invalid outlet ID") + validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid outlet ID") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "OutletHandler::GetOutlet") + return + } + + outletResponse := h.outletService.GetOutletByID(ctx, contextInfo.OrganizationID, outletID) + if outletResponse.HasErrors() { + errorResp := outletResponse.GetErrors()[0] + logger.FromContext(ctx).WithError(errorResp).Error("OutletHandler::GetOutlet -> Failed to get outlet from service") + } + + util.HandleResponse(c.Writer, c.Request, outletResponse, "OutletHandler::GetOutlet") +} + +func (h *OutletHandler) UpdateOutlet(c *gin.Context) { + ctx := c.Request.Context() + contextInfo := appcontext.FromGinContext(ctx) + + outletIDStr := c.Param("id") + outletID, err := uuid.Parse(outletIDStr) + if err != nil { + logger.FromContext(ctx).WithError(err).Error("OutletHandler::UpdateOutlet -> Invalid outlet ID") + validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid outlet ID") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "OutletHandler::UpdateOutlet") + return + } + + var req contract.UpdateOutletRequest + if err := c.ShouldBindJSON(&req); err != nil { + logger.FromContext(ctx).WithError(err).Error("OutletHandler::UpdateOutlet -> Failed to bind JSON") + validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid request body") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "OutletHandler::UpdateOutlet") + return + } + + req.OrganizationID = contextInfo.OrganizationID + + validationError, validationErrorCode := h.outletValidator.ValidateUpdateOutletRequest(&req) + if validationError != nil { + logger.FromContext(ctx).WithError(validationError).Error("OutletHandler::UpdateOutlet -> request validation failed") + validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "OutletHandler::UpdateOutlet") + return + } + + outletResponse := h.outletService.UpdateOutlet(ctx, outletID, &req) + if outletResponse.HasErrors() { + errorResp := outletResponse.GetErrors()[0] + logger.FromContext(ctx).WithError(errorResp).Error("OutletHandler::UpdateOutlet -> Failed to update outlet from service") + } + + util.HandleResponse(c.Writer, c.Request, outletResponse, "OutletHandler::UpdateOutlet") +} diff --git a/internal/handler/outlet_setting_handler.go b/internal/handler/outlet_setting_handler.go new file mode 100644 index 0000000..f523ad2 --- /dev/null +++ b/internal/handler/outlet_setting_handler.go @@ -0,0 +1,226 @@ +package handler + +import ( + "apskel-pos-be/internal/contract" + "apskel-pos-be/internal/models" + "apskel-pos-be/internal/service" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +type OutletSettingHandlerImpl struct { + outletSettingService service.OutletSettingService +} + +func NewOutletSettingHandlerImpl(outletSettingService service.OutletSettingService) *OutletSettingHandlerImpl { + return &OutletSettingHandlerImpl{ + outletSettingService: outletSettingService, + } +} + +func (h *OutletSettingHandlerImpl) CreateSetting(c *gin.Context) { + var req models.CreateOutletSettingRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, contract.ErrorResponse{ + Error: "Invalid request body", + Message: err.Error(), + }) + return + } + + outletID, err := uuid.Parse(c.Param("outlet_id")) + if err != nil { + c.JSON(http.StatusBadRequest, contract.ErrorResponse{ + Error: "Invalid outlet ID", + Message: "Outlet ID must be a valid UUID", + }) + return + } + req.OutletID = outletID + + setting, err := h.outletSettingService.CreateSetting(c.Request.Context(), &req) + if err != nil { + c.JSON(http.StatusInternalServerError, contract.ErrorResponse{ + Error: "Failed to create setting", + Message: err.Error(), + }) + return + } + + c.JSON(http.StatusCreated, contract.SuccessResponse{ + Data: setting, + }) +} + +func (h *OutletSettingHandlerImpl) UpdateSetting(c *gin.Context) { + var req models.UpdateOutletSettingRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, contract.ErrorResponse{ + Error: "Invalid request body", + Message: err.Error(), + }) + return + } + + // Validate outlet ID from URL + outletID, err := uuid.Parse(c.Param("outlet_id")) + if err != nil { + c.JSON(http.StatusBadRequest, contract.ErrorResponse{ + Error: "Invalid outlet ID", + Message: "Outlet ID must be a valid UUID", + }) + return + } + + // Get key from URL + key := c.Param("key") + if key == "" { + c.JSON(http.StatusBadRequest, contract.ErrorResponse{ + Error: "Missing setting key", + Message: "Setting key is required", + }) + return + } + + setting, err := h.outletSettingService.UpdateSetting(c.Request.Context(), outletID, key, &req) + if err != nil { + c.JSON(http.StatusInternalServerError, contract.ErrorResponse{ + Error: "Failed to update setting", + Message: err.Error(), + }) + return + } + + c.JSON(http.StatusOK, contract.SuccessResponse{ + Data: setting, + }) +} + +func (h *OutletSettingHandlerImpl) GetSetting(c *gin.Context) { + outletID, err := uuid.Parse(c.Param("outlet_id")) + if err != nil { + c.JSON(http.StatusBadRequest, contract.ErrorResponse{ + Error: "Invalid outlet ID", + Message: "Outlet ID must be a valid UUID", + }) + return + } + + // Get key from URL + key := c.Param("key") + if key == "" { + c.JSON(http.StatusBadRequest, contract.ErrorResponse{ + Error: "Missing setting key", + Message: "Setting key is required", + }) + return + } + + setting, err := h.outletSettingService.GetSetting(c.Request.Context(), outletID, key) + if err != nil { + c.JSON(http.StatusNotFound, contract.ErrorResponse{ + Error: "Setting not found", + Message: err.Error(), + }) + return + } + + c.JSON(http.StatusOK, contract.SuccessResponse{ + Data: setting, + }) +} + +func (h *OutletSettingHandlerImpl) GetPrinterSettings(c *gin.Context) { + outletID, err := uuid.Parse(c.Param("outlet_id")) + if err != nil { + c.JSON(http.StatusBadRequest, contract.ErrorResponse{ + Error: "Invalid outlet ID", + Message: "Outlet ID must be a valid UUID", + }) + return + } + + settings, err := h.outletSettingService.GetPrinterSettings(c.Request.Context(), outletID) + if err != nil { + c.JSON(http.StatusInternalServerError, contract.ErrorResponse{ + Error: "Failed to get printer settings", + Message: err.Error(), + }) + return + } + + c.JSON(http.StatusOK, contract.SuccessResponse{ + Data: settings, + }) +} + +func (h *OutletSettingHandlerImpl) UpdatePrinterSettings(c *gin.Context) { + var req models.UpdateOutletPrinterSettingsRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, contract.ErrorResponse{ + Error: "Invalid request body", + Message: err.Error(), + }) + return + } + + outletID, err := uuid.Parse(c.Param("outlet_id")) + if err != nil { + c.JSON(http.StatusBadRequest, contract.ErrorResponse{ + Error: "Invalid outlet ID", + Message: "Outlet ID must be a valid UUID", + }) + return + } + + settings, err := h.outletSettingService.UpdatePrinterSettings(c.Request.Context(), outletID, &req) + if err != nil { + c.JSON(http.StatusInternalServerError, contract.ErrorResponse{ + Error: "Failed to update printer settings", + Message: err.Error(), + }) + return + } + + c.JSON(http.StatusOK, contract.SuccessResponse{ + Data: settings, + }) +} + +// DeleteSetting deletes a specific outlet setting +func (h *OutletSettingHandlerImpl) DeleteSetting(c *gin.Context) { + // Validate outlet ID from URL + outletID, err := uuid.Parse(c.Param("outlet_id")) + if err != nil { + c.JSON(http.StatusBadRequest, contract.ErrorResponse{ + Error: "Invalid outlet ID", + Message: "Outlet ID must be a valid UUID", + }) + return + } + + // Get key from URL + key := c.Param("key") + if key == "" { + c.JSON(http.StatusBadRequest, contract.ErrorResponse{ + Error: "Missing setting key", + Message: "Setting key is required", + }) + return + } + + err = h.outletSettingService.DeleteSetting(c.Request.Context(), outletID, key) + if err != nil { + c.JSON(http.StatusInternalServerError, contract.ErrorResponse{ + Error: "Failed to delete setting", + Message: err.Error(), + }) + return + } + + c.JSON(http.StatusOK, contract.SuccessResponse{ + Message: "Setting deleted successfully", + }) +} diff --git a/internal/handler/payment_method_handler.go b/internal/handler/payment_method_handler.go new file mode 100644 index 0000000..571e0d9 --- /dev/null +++ b/internal/handler/payment_method_handler.go @@ -0,0 +1,216 @@ +package handler + +import ( + "strconv" + + "apskel-pos-be/internal/appcontext" + "apskel-pos-be/internal/constants" + "apskel-pos-be/internal/contract" + "apskel-pos-be/internal/logger" + "apskel-pos-be/internal/service" + "apskel-pos-be/internal/util" + "apskel-pos-be/internal/validator" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +type PaymentMethodHandler struct { + paymentMethodService service.PaymentMethodService + paymentMethodValidator validator.PaymentMethodValidator +} + +func NewPaymentMethodHandler( + paymentMethodService service.PaymentMethodService, + paymentMethodValidator validator.PaymentMethodValidator, +) *PaymentMethodHandler { + return &PaymentMethodHandler{ + paymentMethodService: paymentMethodService, + paymentMethodValidator: paymentMethodValidator, + } +} + +func (h *PaymentMethodHandler) CreatePaymentMethod(c *gin.Context) { + ctx := c.Request.Context() + contextInfo := appcontext.FromGinContext(ctx) + + var req contract.CreatePaymentMethodRequest + if err := c.ShouldBindJSON(&req); err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("PaymentMethodHandler::CreatePaymentMethod -> request binding failed") + validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "PaymentMethodHandler::CreatePaymentMethod") + return + } + + validationError, validationErrorCode := h.paymentMethodValidator.ValidateCreatePaymentMethodRequest(&req) + if validationError != nil { + validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "PaymentMethodHandler::CreatePaymentMethod") + return + } + + paymentMethodResponse := h.paymentMethodService.CreatePaymentMethod(ctx, contextInfo, &req) + if paymentMethodResponse.HasErrors() { + errorResp := paymentMethodResponse.GetErrors()[0] + logger.FromContext(ctx).WithError(errorResp).Error("PaymentMethodHandler::CreatePaymentMethod -> Failed to create payment method from service") + } + + util.HandleResponse(c.Writer, c.Request, paymentMethodResponse, "PaymentMethodHandler::CreatePaymentMethod") +} + +func (h *PaymentMethodHandler) GetPaymentMethod(c *gin.Context) { + ctx := c.Request.Context() + + paymentMethodIDStr := c.Param("id") + paymentMethodID, err := uuid.Parse(paymentMethodIDStr) + if err != nil { + logger.FromContext(ctx).WithError(err).Error("PaymentMethodHandler::GetPaymentMethod -> Invalid payment method ID") + validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid payment method ID") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "PaymentMethodHandler::GetPaymentMethod") + return + } + + paymentMethodResponse := h.paymentMethodService.GetPaymentMethodByID(ctx, paymentMethodID) + if paymentMethodResponse.HasErrors() { + errorResp := paymentMethodResponse.GetErrors()[0] + logger.FromContext(ctx).WithError(errorResp).Error("PaymentMethodHandler::GetPaymentMethod -> Failed to get payment method from service") + } + + util.HandleResponse(c.Writer, c.Request, paymentMethodResponse, "PaymentMethodHandler::GetPaymentMethod") +} + +func (h *PaymentMethodHandler) ListPaymentMethods(c *gin.Context) { + ctx := c.Request.Context() + + req := &contract.ListPaymentMethodsRequest{ + Page: 1, + Limit: 10, + } + + // Parse query parameters + if pageStr := c.Query("page"); pageStr != "" { + if page, err := strconv.Atoi(pageStr); err == nil { + req.Page = page + } + } + + if limitStr := c.Query("limit"); limitStr != "" { + if limit, err := strconv.Atoi(limitStr); err == nil { + req.Limit = limit + } + } + + if search := c.Query("search"); search != "" { + req.Search = search + } + + if paymentMethodType := c.Query("type"); paymentMethodType != "" { + req.Type = &paymentMethodType + } + + if organizationIDStr := c.Query("organization_id"); organizationIDStr != "" { + if organizationID, err := uuid.Parse(organizationIDStr); err == nil { + req.OrganizationID = &organizationID + } + } + + if isActiveStr := c.Query("is_active"); isActiveStr != "" { + if isActive, err := strconv.ParseBool(isActiveStr); err == nil { + req.IsActive = &isActive + } + } + + validationError, validationErrorCode := h.paymentMethodValidator.ValidateListPaymentMethodsRequest(req) + if validationError != nil { + logger.FromContext(ctx).WithError(validationError).Error("PaymentMethodHandler::ListPaymentMethods -> request validation failed") + validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "PaymentMethodHandler::ListPaymentMethods") + return + } + + paymentMethodsResponse := h.paymentMethodService.ListPaymentMethods(ctx, req) + if paymentMethodsResponse.HasErrors() { + errorResp := paymentMethodsResponse.GetErrors()[0] + logger.FromContext(ctx).WithError(errorResp).Error("PaymentMethodHandler::ListPaymentMethods -> Failed to list payment methods from service") + } + + util.HandleResponse(c.Writer, c.Request, paymentMethodsResponse, "PaymentMethodHandler::ListPaymentMethods") +} + +func (h *PaymentMethodHandler) UpdatePaymentMethod(c *gin.Context) { + ctx := c.Request.Context() + + paymentMethodIDStr := c.Param("id") + paymentMethodID, err := uuid.Parse(paymentMethodIDStr) + if err != nil { + logger.FromContext(ctx).WithError(err).Error("PaymentMethodHandler::UpdatePaymentMethod -> Invalid payment method ID") + validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid payment method ID") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "PaymentMethodHandler::UpdatePaymentMethod") + return + } + + var req contract.UpdatePaymentMethodRequest + if err := c.ShouldBindJSON(&req); err != nil { + logger.FromContext(ctx).WithError(err).Error("PaymentMethodHandler::UpdatePaymentMethod -> request binding failed") + validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, "Invalid request body") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "PaymentMethodHandler::UpdatePaymentMethod") + return + } + + validationError, validationErrorCode := h.paymentMethodValidator.ValidateUpdatePaymentMethodRequest(&req) + if validationError != nil { + validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "PaymentMethodHandler::UpdatePaymentMethod") + return + } + + paymentMethodResponse := h.paymentMethodService.UpdatePaymentMethod(ctx, paymentMethodID, &req) + if paymentMethodResponse.HasErrors() { + errorResp := paymentMethodResponse.GetErrors()[0] + logger.FromContext(ctx).WithError(errorResp).Error("PaymentMethodHandler::UpdatePaymentMethod -> Failed to update payment method from service") + } + + util.HandleResponse(c.Writer, c.Request, paymentMethodResponse, "PaymentMethodHandler::UpdatePaymentMethod") +} + +func (h *PaymentMethodHandler) DeletePaymentMethod(c *gin.Context) { + ctx := c.Request.Context() + + paymentMethodIDStr := c.Param("id") + paymentMethodID, err := uuid.Parse(paymentMethodIDStr) + if err != nil { + logger.FromContext(ctx).WithError(err).Error("PaymentMethodHandler::DeletePaymentMethod -> Invalid payment method ID") + validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid payment method ID") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "PaymentMethodHandler::DeletePaymentMethod") + return + } + + paymentMethodResponse := h.paymentMethodService.DeletePaymentMethod(ctx, paymentMethodID) + if paymentMethodResponse.HasErrors() { + errorResp := paymentMethodResponse.GetErrors()[0] + logger.FromContext(ctx).WithError(errorResp).Error("PaymentMethodHandler::DeletePaymentMethod -> Failed to delete payment method from service") + } + + util.HandleResponse(c.Writer, c.Request, paymentMethodResponse, "PaymentMethodHandler::DeletePaymentMethod") +} + +func (h *PaymentMethodHandler) GetActivePaymentMethodsByOrganization(c *gin.Context) { + ctx := c.Request.Context() + + organizationIDStr := c.Param("organization_id") + organizationID, err := uuid.Parse(organizationIDStr) + if err != nil { + logger.FromContext(ctx).WithError(err).Error("PaymentMethodHandler::GetActivePaymentMethodsByOrganization -> Invalid organization ID") + validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid organization ID") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "PaymentMethodHandler::GetActivePaymentMethodsByOrganization") + return + } + + paymentMethodsResponse := h.paymentMethodService.GetActivePaymentMethodsByOrganization(ctx, organizationID) + if paymentMethodsResponse.HasErrors() { + errorResp := paymentMethodsResponse.GetErrors()[0] + logger.FromContext(ctx).WithError(errorResp).Error("PaymentMethodHandler::GetActivePaymentMethodsByOrganization -> Failed to get active payment methods from service") + } + + util.HandleResponse(c.Writer, c.Request, paymentMethodsResponse, "PaymentMethodHandler::GetActivePaymentMethodsByOrganization") +} diff --git a/internal/handler/product_handler.go b/internal/handler/product_handler.go new file mode 100644 index 0000000..97e7cc2 --- /dev/null +++ b/internal/handler/product_handler.go @@ -0,0 +1,213 @@ +package handler + +import ( + "strconv" + + "apskel-pos-be/internal/appcontext" + "apskel-pos-be/internal/constants" + "apskel-pos-be/internal/contract" + "apskel-pos-be/internal/logger" + "apskel-pos-be/internal/service" + "apskel-pos-be/internal/util" + "apskel-pos-be/internal/validator" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +type ProductHandler struct { + productService service.ProductService + productValidator validator.ProductValidator +} + +func NewProductHandler( + productService service.ProductService, + productValidator validator.ProductValidator, +) *ProductHandler { + return &ProductHandler{ + productService: productService, + productValidator: productValidator, + } +} + +func (h *ProductHandler) CreateProduct(c *gin.Context) { + ctx := c.Request.Context() + contextInfo := appcontext.FromGinContext(ctx) + + var req contract.CreateProductRequest + if err := c.ShouldBindJSON(&req); err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("ProductHandler::CreateProduct -> request binding failed") + validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ProductHandler::CreateProduct") + return + } + + validationError, validationErrorCode := h.productValidator.ValidateCreateProductRequest(&req) + if validationError != nil { + validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ProductHandler::CreateProduct") + return + } + + productResponse := h.productService.CreateProduct(ctx, contextInfo, &req) + if productResponse.HasErrors() { + errorResp := productResponse.GetErrors()[0] + logger.FromContext(ctx).WithError(errorResp).Error("ProductHandler::CreateProduct -> Failed to create product from service") + } + + util.HandleResponse(c.Writer, c.Request, productResponse, "ProductHandler::CreateProduct") +} + +func (h *ProductHandler) UpdateProduct(c *gin.Context) { + ctx := c.Request.Context() + + productIDStr := c.Param("id") + productID, err := uuid.Parse(productIDStr) + if err != nil { + logger.FromContext(ctx).WithError(err).Error("ProductHandler::UpdateProduct -> Invalid product ID") + validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid product ID") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ProductHandler::UpdateProduct") + return + } + + var req contract.UpdateProductRequest + if err := c.ShouldBindJSON(&req); err != nil { + logger.FromContext(ctx).WithError(err).Error("ProductHandler::UpdateProduct -> request binding failed") + validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, "Invalid request body") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ProductHandler::UpdateProduct") + return + } + + validationError, validationErrorCode := h.productValidator.ValidateUpdateProductRequest(&req) + if validationError != nil { + validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ProductHandler::UpdateProduct") + return + } + + productResponse := h.productService.UpdateProduct(ctx, productID, &req) + if productResponse.HasErrors() { + errorResp := productResponse.GetErrors()[0] + logger.FromContext(ctx).WithError(errorResp).Error("ProductHandler::UpdateProduct -> Failed to update product from service") + } + + util.HandleResponse(c.Writer, c.Request, productResponse, "ProductHandler::UpdateProduct") +} + +func (h *ProductHandler) DeleteProduct(c *gin.Context) { + ctx := c.Request.Context() + + productIDStr := c.Param("id") + productID, err := uuid.Parse(productIDStr) + if err != nil { + logger.FromContext(ctx).WithError(err).Error("ProductHandler::DeleteProduct -> Invalid product ID") + validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid product ID") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ProductHandler::DeleteProduct") + return + } + + productResponse := h.productService.DeleteProduct(ctx, productID) + if productResponse.HasErrors() { + errorResp := productResponse.GetErrors()[0] + logger.FromContext(ctx).WithError(errorResp).Error("ProductHandler::DeleteProduct -> Failed to delete product from service") + } + + util.HandleResponse(c.Writer, c.Request, productResponse, "ProductHandler::DeleteProduct") +} + +func (h *ProductHandler) GetProduct(c *gin.Context) { + ctx := c.Request.Context() + + productIDStr := c.Param("id") + productID, err := uuid.Parse(productIDStr) + if err != nil { + logger.FromContext(ctx).WithError(err).Error("ProductHandler::GetProduct -> Invalid product ID") + validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid product ID") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ProductHandler::GetProduct") + return + } + + productResponse := h.productService.GetProductByID(ctx, productID) + if productResponse.HasErrors() { + errorResp := productResponse.GetErrors()[0] + logger.FromContext(ctx).WithError(errorResp).Error("ProductHandler::GetProduct -> Failed to get product from service") + } + + util.HandleResponse(c.Writer, c.Request, productResponse, "ProductHandler::GetProduct") +} + +func (h *ProductHandler) ListProducts(c *gin.Context) { + ctx := c.Request.Context() + + req := &contract.ListProductsRequest{ + Page: 1, + Limit: 10, + } + + // Parse query parameters + if pageStr := c.Query("page"); pageStr != "" { + if page, err := strconv.Atoi(pageStr); err == nil { + req.Page = page + } + } + + if limitStr := c.Query("limit"); limitStr != "" { + if limit, err := strconv.Atoi(limitStr); err == nil { + req.Limit = limit + } + } + + if search := c.Query("search"); search != "" { + req.Search = search + } + + if businessType := c.Query("business_type"); businessType != "" { + req.BusinessType = businessType + } + + if organizationIDStr := c.Query("organization_id"); organizationIDStr != "" { + if organizationID, err := uuid.Parse(organizationIDStr); err == nil { + req.OrganizationID = &organizationID + } + } + + if categoryIDStr := c.Query("category_id"); categoryIDStr != "" { + if categoryID, err := uuid.Parse(categoryIDStr); err == nil { + req.CategoryID = &categoryID + } + } + + if isActiveStr := c.Query("is_active"); isActiveStr != "" { + if isActive, err := strconv.ParseBool(isActiveStr); err == nil { + req.IsActive = &isActive + } + } + + if minPriceStr := c.Query("min_price"); minPriceStr != "" { + if minPrice, err := strconv.ParseFloat(minPriceStr, 64); err == nil { + req.MinPrice = &minPrice + } + } + + if maxPriceStr := c.Query("max_price"); maxPriceStr != "" { + if maxPrice, err := strconv.ParseFloat(maxPriceStr, 64); err == nil { + req.MaxPrice = &maxPrice + } + } + + validationError, validationErrorCode := h.productValidator.ValidateListProductsRequest(req) + if validationError != nil { + logger.FromContext(ctx).WithError(validationError).Error("ProductHandler::ListProducts -> request validation failed") + validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ProductHandler::ListProducts") + return + } + + productsResponse := h.productService.ListProducts(ctx, req) + if productsResponse.HasErrors() { + errorResp := productsResponse.GetErrors()[0] + logger.FromContext(ctx).WithError(errorResp).Error("ProductHandler::ListProducts -> Failed to list products from service") + } + + util.HandleResponse(c.Writer, c.Request, productsResponse, "ProductHandler::ListProducts") +} diff --git a/internal/handler/product_variant_handler.go b/internal/handler/product_variant_handler.go new file mode 100644 index 0000000..20a5012 --- /dev/null +++ b/internal/handler/product_variant_handler.go @@ -0,0 +1,156 @@ +package handler + +import ( + "apskel-pos-be/internal/appcontext" + "apskel-pos-be/internal/constants" + "apskel-pos-be/internal/contract" + "apskel-pos-be/internal/logger" + "apskel-pos-be/internal/service" + "apskel-pos-be/internal/util" + "apskel-pos-be/internal/validator" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +type ProductVariantHandler struct { + productVariantService service.ProductVariantService + productVariantValidator validator.ProductVariantValidator +} + +func NewProductVariantHandler( + productVariantService service.ProductVariantService, + productVariantValidator validator.ProductVariantValidator, +) *ProductVariantHandler { + return &ProductVariantHandler{ + productVariantService: productVariantService, + productVariantValidator: productVariantValidator, + } +} + +func (h *ProductVariantHandler) CreateProductVariant(c *gin.Context) { + ctx := c.Request.Context() + contextInfo := appcontext.FromGinContext(ctx) + + var req contract.CreateProductVariantRequest + if err := c.ShouldBindJSON(&req); err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("ProductVariantHandler::CreateProductVariant -> request binding failed") + validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ProductVariantHandler::CreateProductVariant") + return + } + + validationError, validationErrorCode := h.productVariantValidator.ValidateCreateProductVariantRequest(&req) + if validationError != nil { + validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ProductVariantHandler::CreateProductVariant") + return + } + + variantResponse := h.productVariantService.CreateProductVariant(ctx, contextInfo, &req) + if variantResponse.HasErrors() { + errorResp := variantResponse.GetErrors()[0] + logger.FromContext(ctx).WithError(errorResp).Error("ProductVariantHandler::CreateProductVariant -> Failed to create product variant from service") + } + + util.HandleResponse(c.Writer, c.Request, variantResponse, "ProductVariantHandler::CreateProductVariant") +} + +func (h *ProductVariantHandler) UpdateProductVariant(c *gin.Context) { + ctx := c.Request.Context() + + variantIDStr := c.Param("id") + variantID, err := uuid.Parse(variantIDStr) + if err != nil { + logger.FromContext(ctx).WithError(err).Error("ProductVariantHandler::UpdateProductVariant -> Invalid variant ID") + validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid variant ID") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ProductVariantHandler::UpdateProductVariant") + return + } + + var req contract.UpdateProductVariantRequest + if err := c.ShouldBindJSON(&req); err != nil { + logger.FromContext(ctx).WithError(err).Error("ProductVariantHandler::UpdateProductVariant -> request binding failed") + validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, "Invalid request body") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ProductVariantHandler::UpdateProductVariant") + return + } + + validationError, validationErrorCode := h.productVariantValidator.ValidateUpdateProductVariantRequest(&req) + if validationError != nil { + validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ProductVariantHandler::UpdateProductVariant") + return + } + + variantResponse := h.productVariantService.UpdateProductVariant(ctx, variantID, &req) + if variantResponse.HasErrors() { + errorResp := variantResponse.GetErrors()[0] + logger.FromContext(ctx).WithError(errorResp).Error("ProductVariantHandler::UpdateProductVariant -> Failed to update product variant from service") + } + + util.HandleResponse(c.Writer, c.Request, variantResponse, "ProductVariantHandler::UpdateProductVariant") +} + +func (h *ProductVariantHandler) DeleteProductVariant(c *gin.Context) { + ctx := c.Request.Context() + + variantIDStr := c.Param("id") + variantID, err := uuid.Parse(variantIDStr) + if err != nil { + logger.FromContext(ctx).WithError(err).Error("ProductVariantHandler::DeleteProductVariant -> Invalid variant ID") + validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid variant ID") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ProductVariantHandler::DeleteProductVariant") + return + } + + variantResponse := h.productVariantService.DeleteProductVariant(ctx, variantID) + if variantResponse.HasErrors() { + errorResp := variantResponse.GetErrors()[0] + logger.FromContext(ctx).WithError(errorResp).Error("ProductVariantHandler::DeleteProductVariant -> Failed to delete product variant from service") + } + + util.HandleResponse(c.Writer, c.Request, variantResponse, "ProductVariantHandler::DeleteProductVariant") +} + +func (h *ProductVariantHandler) GetProductVariant(c *gin.Context) { + ctx := c.Request.Context() + + variantIDStr := c.Param("id") + variantID, err := uuid.Parse(variantIDStr) + if err != nil { + logger.FromContext(ctx).WithError(err).Error("ProductVariantHandler::GetProductVariant -> Invalid variant ID") + validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid variant ID") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ProductVariantHandler::GetProductVariant") + return + } + + variantResponse := h.productVariantService.GetProductVariantByID(ctx, variantID) + if variantResponse.HasErrors() { + errorResp := variantResponse.GetErrors()[0] + logger.FromContext(ctx).WithError(errorResp).Error("ProductVariantHandler::GetProductVariant -> Failed to get product variant from service") + } + + util.HandleResponse(c.Writer, c.Request, variantResponse, "ProductVariantHandler::GetProductVariant") +} + +func (h *ProductVariantHandler) GetProductVariantsByProduct(c *gin.Context) { + ctx := c.Request.Context() + + productIDStr := c.Param("product_id") + productID, err := uuid.Parse(productIDStr) + if err != nil { + logger.FromContext(ctx).WithError(err).Error("ProductVariantHandler::GetProductVariantsByProduct -> Invalid product ID") + validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid product ID") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ProductVariantHandler::GetProductVariantsByProduct") + return + } + + variantsResponse := h.productVariantService.GetProductVariantsByProductID(ctx, productID) + if variantsResponse.HasErrors() { + errorResp := variantsResponse.GetErrors()[0] + logger.FromContext(ctx).WithError(errorResp).Error("ProductVariantHandler::GetProductVariantsByProduct -> Failed to get product variants from service") + } + + util.HandleResponse(c.Writer, c.Request, variantsResponse, "ProductVariantHandler::GetProductVariantsByProduct") +} diff --git a/internal/handler/user_handler.go b/internal/handler/user_handler.go new file mode 100644 index 0000000..1ba681d --- /dev/null +++ b/internal/handler/user_handler.go @@ -0,0 +1,319 @@ +package handler + +import ( + "net/http" + "strconv" + + "apskel-pos-be/internal/constants" + "apskel-pos-be/internal/contract" + "apskel-pos-be/internal/logger" + "apskel-pos-be/internal/transformer" + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +type UserHandler struct { + userService UserService + userValidator UserValidator +} + +func NewUserHandler(userService UserService, userValidator UserValidator) *UserHandler { + return &UserHandler{ + userService: userService, + userValidator: userValidator, + } +} + +func (h *UserHandler) CreateUser(c *gin.Context) { + var req contract.CreateUserRequest + if err := c.ShouldBindJSON(&req); err != nil { + logger.FromContext(c).WithError(err).Error("UserHandler::CreateUser -> request binding failed") + h.sendValidationErrorResponse(c, "Invalid request body", constants.MissingFieldErrorCode) + return + } + + validationError, validationErrorCode := h.userValidator.ValidateCreateUserRequest(&req) + if validationError != nil { + logger.FromContext(c).WithError(validationError).Error("UserHandler::CreateUser -> request validation failed") + h.sendValidationErrorResponse(c, validationError.Error(), validationErrorCode) + return + } + + userResponse, err := h.userService.CreateUser(c.Request.Context(), &req) + if err != nil { + logger.FromContext(c).WithError(err).Error("UserHandler::CreateUser -> Failed to create user from service") + h.sendErrorResponse(c, err.Error(), http.StatusInternalServerError) + return + } + + logger.FromContext(c).Infof("UserHandler::CreateUser -> Successfully created user = %+v", userResponse) + c.JSON(http.StatusCreated, userResponse) +} + +func (h *UserHandler) UpdateUser(c *gin.Context) { + userIDStr := c.Param("id") + userID, err := uuid.Parse(userIDStr) + if err != nil { + logger.FromContext(c).WithError(err).Error("UserHandler::UpdateUser -> Invalid user ID") + h.sendValidationErrorResponse(c, "Invalid user ID", constants.MalformedFieldErrorCode) + return + } + + validationError, validationErrorCode := h.userValidator.ValidateUserID(userID) + if validationError != nil { + logger.FromContext(c).WithError(validationError).Error("UserHandler::UpdateUser -> user ID validation failed") + h.sendValidationErrorResponse(c, validationError.Error(), validationErrorCode) + return + } + + var req contract.UpdateUserRequest + if err := c.ShouldBindJSON(&req); err != nil { + logger.FromContext(c).WithError(err).Error("UserHandler::UpdateUser -> request binding failed") + h.sendValidationErrorResponse(c, "Invalid request body", constants.MissingFieldErrorCode) + return + } + + validationError, validationErrorCode = h.userValidator.ValidateUpdateUserRequest(&req) + if validationError != nil { + logger.FromContext(c).WithError(validationError).Error("UserHandler::UpdateUser -> request validation failed") + h.sendValidationErrorResponse(c, validationError.Error(), validationErrorCode) + return + } + + userResponse, err := h.userService.UpdateUser(c.Request.Context(), userID, &req) + if err != nil { + logger.FromContext(c).WithError(err).Error("UserHandler::UpdateUser -> Failed to update user from service") + h.sendErrorResponse(c, err.Error(), http.StatusInternalServerError) + return + } + + logger.FromContext(c).Infof("UserHandler::UpdateUser -> Successfully updated user = %+v", userResponse) + c.JSON(http.StatusOK, userResponse) +} + +func (h *UserHandler) DeleteUser(c *gin.Context) { + userIDStr := c.Param("id") + userID, err := uuid.Parse(userIDStr) + if err != nil { + logger.FromContext(c).WithError(err).Error("UserHandler::DeleteUser -> Invalid user ID") + h.sendValidationErrorResponse(c, "Invalid user ID", constants.MalformedFieldErrorCode) + return + } + + validationError, validationErrorCode := h.userValidator.ValidateUserID(userID) + if validationError != nil { + logger.FromContext(c).WithError(validationError).Error("UserHandler::DeleteUser -> user ID validation failed") + h.sendValidationErrorResponse(c, validationError.Error(), validationErrorCode) + return + } + + err = h.userService.DeleteUser(c.Request.Context(), userID) + if err != nil { + logger.FromContext(c).WithError(err).Error("UserHandler::DeleteUser -> Failed to delete user from service") + h.sendErrorResponse(c, err.Error(), http.StatusInternalServerError) + return + } + + logger.FromContext(c).Info("UserHandler::DeleteUser -> Successfully deleted user") + c.JSON(http.StatusOK, transformer.CreateSuccessResponse("User deleted successfully", nil)) +} + +func (h *UserHandler) GetUser(c *gin.Context) { + userIDStr := c.Param("id") + userID, err := uuid.Parse(userIDStr) + if err != nil { + logger.FromContext(c).WithError(err).Error("UserHandler::GetUser -> Invalid user ID") + h.sendValidationErrorResponse(c, "Invalid user ID", constants.MalformedFieldErrorCode) + return + } + + validationError, validationErrorCode := h.userValidator.ValidateUserID(userID) + if validationError != nil { + logger.FromContext(c).WithError(validationError).Error("UserHandler::GetUser -> user ID validation failed") + h.sendValidationErrorResponse(c, validationError.Error(), validationErrorCode) + return + } + + userResponse, err := h.userService.GetUserByID(c.Request.Context(), userID) + if err != nil { + logger.FromContext(c).WithError(err).Error("UserHandler::GetUser -> Failed to get user from service") + h.sendErrorResponse(c, err.Error(), http.StatusInternalServerError) + return + } + + logger.FromContext(c).Infof("UserHandler::GetUser -> Successfully retrieved user = %+v", userResponse) + c.JSON(http.StatusOK, userResponse) +} + +func (h *UserHandler) ListUsers(c *gin.Context) { + req := &contract.ListUsersRequest{ + Page: 1, + Limit: 10, + } + + if page := c.Query("page"); page != "" { + if p, err := strconv.Atoi(page); err == nil { + req.Page = p + } + } + + if limit := c.Query("limit"); limit != "" { + if l, err := strconv.Atoi(limit); err == nil { + req.Limit = l + } + } + + if role := c.Query("role"); role != "" { + req.Role = &role + } + + if outletIDStr := c.Query("outlet_id"); outletIDStr != "" { + if outletID, err := uuid.Parse(outletIDStr); err == nil { + req.OutletID = &outletID + } + } + + if isActiveStr := c.Query("is_active"); isActiveStr != "" { + if isActive, err := strconv.ParseBool(isActiveStr); err == nil { + req.IsActive = &isActive + } + } + + validationError, validationErrorCode := h.userValidator.ValidateListUsersRequest(req) + if validationError != nil { + logger.FromContext(c).WithError(validationError).Error("UserHandler::ListUsers -> request validation failed") + h.sendValidationErrorResponse(c, validationError.Error(), validationErrorCode) + return + } + + usersResponse, err := h.userService.ListUsers(c.Request.Context(), req) + if err != nil { + logger.FromContext(c).WithError(err).Error("UserHandler::ListUsers -> Failed to list users from service") + h.sendErrorResponse(c, err.Error(), http.StatusInternalServerError) + return + } + + logger.FromContext(c).Infof("UserHandler::ListUsers -> Successfully listed users = %+v", usersResponse) + c.JSON(http.StatusOK, contract.BuildSuccessResponse(usersResponse)) +} + +func (h *UserHandler) ChangePassword(c *gin.Context) { + userIDStr := c.Param("id") + userID, err := uuid.Parse(userIDStr) + if err != nil { + logger.FromContext(c).WithError(err).Error("UserHandler::ChangePassword -> Invalid user ID") + h.sendValidationErrorResponse(c, "Invalid user ID", constants.MalformedFieldErrorCode) + return + } + + validationError, validationErrorCode := h.userValidator.ValidateUserID(userID) + if validationError != nil { + logger.FromContext(c).WithError(validationError).Error("UserHandler::ChangePassword -> user ID validation failed") + h.sendValidationErrorResponse(c, validationError.Error(), validationErrorCode) + return + } + + var req contract.ChangePasswordRequest + if err := c.ShouldBindJSON(&req); err != nil { + logger.FromContext(c).WithError(err).Error("UserHandler::ChangePassword -> request binding failed") + h.sendValidationErrorResponse(c, "Invalid request body", constants.MissingFieldErrorCode) + return + } + + validationError, validationErrorCode = h.userValidator.ValidateChangePasswordRequest(&req) + if validationError != nil { + logger.FromContext(c).WithError(validationError).Error("UserHandler::ChangePassword -> request validation failed") + h.sendValidationErrorResponse(c, validationError.Error(), validationErrorCode) + return + } + + err = h.userService.ChangePassword(c.Request.Context(), userID, &req) + if err != nil { + logger.FromContext(c).WithError(err).Error("UserHandler::ChangePassword -> Failed to change password from service") + h.sendErrorResponse(c, err.Error(), http.StatusInternalServerError) + return + } + + logger.FromContext(c).Info("UserHandler::ChangePassword -> Successfully changed password") + c.JSON(http.StatusOK, transformer.CreateSuccessResponse("Password changed successfully", nil)) +} + +func (h *UserHandler) ActivateUser(c *gin.Context) { + userIDStr := c.Param("id") + userID, err := uuid.Parse(userIDStr) + if err != nil { + logger.FromContext(c).WithError(err).Error("UserHandler::ActivateUser -> Invalid user ID") + h.sendValidationErrorResponse(c, "Invalid user ID", constants.MalformedFieldErrorCode) + return + } + + validationError, validationErrorCode := h.userValidator.ValidateUserID(userID) + if validationError != nil { + logger.FromContext(c).WithError(validationError).Error("UserHandler::ActivateUser -> user ID validation failed") + h.sendValidationErrorResponse(c, validationError.Error(), validationErrorCode) + return + } + + err = h.userService.ActivateUser(c.Request.Context(), userID) + if err != nil { + logger.FromContext(c).WithError(err).Error("UserHandler::ActivateUser -> Failed to activate user from service") + h.sendErrorResponse(c, err.Error(), http.StatusInternalServerError) + return + } + + logger.FromContext(c).Info("UserHandler::ActivateUser -> Successfully activated user") + c.JSON(http.StatusOK, transformer.CreateSuccessResponse("User activated successfully", nil)) +} + +func (h *UserHandler) DeactivateUser(c *gin.Context) { + userIDStr := c.Param("id") + userID, err := uuid.Parse(userIDStr) + if err != nil { + logger.FromContext(c).WithError(err).Error("UserHandler::DeactivateUser -> Invalid user ID") + h.sendValidationErrorResponse(c, "Invalid user ID", constants.MalformedFieldErrorCode) + return + } + + validationError, validationErrorCode := h.userValidator.ValidateUserID(userID) + if validationError != nil { + logger.FromContext(c).WithError(validationError).Error("UserHandler::DeactivateUser -> user ID validation failed") + h.sendValidationErrorResponse(c, validationError.Error(), validationErrorCode) + return + } + + err = h.userService.DeactivateUser(c.Request.Context(), userID) + if err != nil { + logger.FromContext(c).WithError(err).Error("UserHandler::DeactivateUser -> Failed to deactivate user from service") + h.sendErrorResponse(c, err.Error(), http.StatusInternalServerError) + return + } + + logger.FromContext(c).Info("UserHandler::DeactivateUser -> Successfully deactivated user") + c.JSON(http.StatusOK, transformer.CreateSuccessResponse("User deactivated successfully", nil)) +} + +func (h *UserHandler) sendErrorResponse(c *gin.Context, message string, statusCode int) { + errorResponse := &contract.ErrorResponse{ + Error: message, + Code: statusCode, + Details: map[string]interface{}{}, + } + c.JSON(statusCode, errorResponse) +} + +func (h *UserHandler) sendValidationErrorResponse(c *gin.Context, message string, errorCode string) { + statusCode := constants.HttpErrorMap[errorCode] + if statusCode == 0 { + statusCode = http.StatusBadRequest + } + + errorResponse := &contract.ErrorResponse{ + Error: message, + Code: statusCode, + Details: map[string]interface{}{ + "error_code": errorCode, + "entity": constants.UserValidatorEntity, + }, + } + c.JSON(statusCode, errorResponse) +} diff --git a/internal/handler/user_service.go b/internal/handler/user_service.go new file mode 100644 index 0000000..9b1ec52 --- /dev/null +++ b/internal/handler/user_service.go @@ -0,0 +1,19 @@ +package handler + +import ( + "apskel-pos-be/internal/contract" + "context" + "github.com/google/uuid" +) + +type UserService interface { + CreateUser(ctx context.Context, req *contract.CreateUserRequest) (*contract.UserResponse, error) + UpdateUser(ctx context.Context, id uuid.UUID, req *contract.UpdateUserRequest) (*contract.UserResponse, error) + DeleteUser(ctx context.Context, id uuid.UUID) error + GetUserByID(ctx context.Context, id uuid.UUID) (*contract.UserResponse, error) + GetUserByEmail(ctx context.Context, email string) (*contract.UserResponse, error) + ListUsers(ctx context.Context, req *contract.ListUsersRequest) (*contract.ListUsersResponse, error) + ChangePassword(ctx context.Context, userID uuid.UUID, req *contract.ChangePasswordRequest) error + ActivateUser(ctx context.Context, userID uuid.UUID) error + DeactivateUser(ctx context.Context, userID uuid.UUID) error +} diff --git a/internal/handler/user_validator.go b/internal/handler/user_validator.go new file mode 100644 index 0000000..0c233f0 --- /dev/null +++ b/internal/handler/user_validator.go @@ -0,0 +1,14 @@ +package handler + +import ( + "apskel-pos-be/internal/contract" + "github.com/google/uuid" +) + +type UserValidator interface { + ValidateCreateUserRequest(req *contract.CreateUserRequest) (error, string) + ValidateUpdateUserRequest(req *contract.UpdateUserRequest) (error, string) + ValidateListUsersRequest(req *contract.ListUsersRequest) (error, string) + ValidateChangePasswordRequest(req *contract.ChangePasswordRequest) (error, string) + ValidateUserID(userID uuid.UUID) (error, string) +} diff --git a/internal/handlers/.DS_Store b/internal/handlers/.DS_Store deleted file mode 100644 index 2999c9d6dc0c06025e5e1eb04956a6c8df595cfc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKJ5Iw;5S#@|SfWWu`L4hXo}zF9E`T7BC?Xti(!1l@%!>G%|yH7?8i@@ z!s`WK+spnQ*Z}Bh&iHhi+58^+@#XkD497*~dcb=-yyDE?Y2JT>4;=80 z3;l@mJk>{P#0dl5m^{pr>qjXg1*Cu!kOERb3Qz&gY)60FSY8T90V(jKfWIFK&ABE| zjpNlp7b^g{V>rO;*d@rt1LT@KHD-jc)=F-zmMey}cE(HA)#RyhYlr3XVR^FUhGKC# z?_Z)ERvXJp0V(iX0pGoejnDrF+RFa#HG&k70{>J2>voU3Egvpt>*6te))v|m?HjC( na=IABLX)R#*I*`IX2Nm=LUys<|pZeWK{g=VDjOXla~ zS;}MpxXn2{0BZmhRd8|32(hS$ifSg@{7o0KRb$Wg) z?9kD1LX$g6?D0W8VVK5Oc*6mKWl~l@b(Uyp4$SJN<|WNNvd9~}bJLDxPM6W*>X;Gu zuuZ;s$ubA#?DJwPng>P)I@{^S1&#e>{cAlUdoE+Z7%&EYodNc2kxF`ii7{Xd7z0xV zMg0V%=GD}E~SF9Dsg!E8cN+qULrz3_->9l)|s};wDDIHFS z52rgjolsodo%MYThpQ9XY77_yIRnddyO#d{`1AaqC)u4bU<~{#23)ay+HUwt+FNHY vC%x8FpQ$1eHzxc_;YDo4jFnb=PmMymmkVO8I3}cpVm|_r23w4QKV{$(xO7Yo diff --git a/internal/handlers/http/auth/auth.go b/internal/handlers/http/auth/auth.go deleted file mode 100644 index 18d22b7..0000000 --- a/internal/handlers/http/auth/auth.go +++ /dev/null @@ -1,175 +0,0 @@ -package auth - -import ( - "enaklo-pos-be/internal/constants/role" - "enaklo-pos-be/internal/services/member" - "fmt" - "net/http" - "strings" - - "github.com/gin-gonic/gin" - - "enaklo-pos-be/internal/common/errors" - auth2 "enaklo-pos-be/internal/handlers/request" - "enaklo-pos-be/internal/handlers/response" - "enaklo-pos-be/internal/services" -) - -type AuthHandler struct { - service services.Auth - memberSvc member.RegistrationService -} - -func (a *AuthHandler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) { - authRoute := group.Group("/auth") - authRoute.POST("/login", a.AuthLogin) - authRoute.POST("/forgot-password", a.ForgotPassword) - authRoute.POST("/reset-password", jwt, a.ResetPassword) -} - -func NewAuthHandler(service services.Auth) *AuthHandler { - return &AuthHandler{ - service: service, - } -} - -// AuthLogin handles the authentication process for user login. -// @Summary User login -// @Description Authenticates a user based on the provided credentials and returns a JWT token. -// @Accept json -// @Produce json -// @Param bodyParam body auth2.LoginRequest true "User login credentials" -// @Success 200 {object} response.BaseResponse{data=response.LoginResponse} "Login successful" -// @Failure 400 {object} response.BaseResponse{data=errors.Error} "Bad request" -// @Failure 401 {object} response.BaseResponse{data=errors.Error} "Unauthorized" -// @Router /api/v1/auth/login [post] -// @Tags Auth Login API's -func (h *AuthHandler) AuthLogin(c *gin.Context) { - var bodyParam auth2.LoginRequest - if err := c.ShouldBindJSON(&bodyParam); err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - email := strings.ToLower(bodyParam.Email) - authUser, err := h.service.AuthenticateUser(c, email, bodyParam.Password) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - if authUser.UserType == "CUSTOMER" { - response.ErrorWrapper(c, errors.ErrorUserIsNotFound) - return - } - - var partner *response.Partner - var site *response.SiteName - - if authUser.RoleID != role.SuperAdmin { - partner = &response.Partner{ - ID: authUser.PartnerID, - Name: authUser.PartnerName, - Status: authUser.PartnerStatus, - } - } - - if authUser.RoleID == role.Casheer || authUser.RoleID == role.SiteAdmin { - site = &response.SiteName{ - ID: authUser.SiteID, - Name: authUser.SiteName, - } - } - - resp := response.LoginResponse{ - Token: authUser.Token, - Partner: partner, - Name: authUser.Name, - Role: response.Role{ - ID: int64(authUser.RoleID), - Role: authUser.RoleName, - }, - Site: site, - ResetPassword: authUser.ResetPassword, - PartnerLicense: &response.PartnerLicense{ - DaysToExpire: authUser.PartnerLicense.DaysToExpire, - Status: authUser.PartnerLicense.LicenseStatus, - }, - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Message: "Login Success", - Data: resp, - }) -} - -// ForgotPassword handles the request for password reset. -// @Summary Request password reset -// @Description Sends a password reset link to the user's email. -// @Accept json -// @Produce json -// @Param bodyParam body auth2.ForgotPasswordRequest true "User email" -// @Success 200 {object} response.BaseResponse "Password reset link sent" -// @Failure 400 {object} response.BaseResponse{data=errors.Error} "Bad request" -// @Router /api/v1/auth/forgot-password [post] -// @Tags Auth Password API's -func (h *AuthHandler) ForgotPassword(c *gin.Context) { - var bodyParam auth2.ResetPasswordRequest - if err := c.ShouldBindJSON(&bodyParam); err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - err := h.service.SendPasswordResetLink(c, bodyParam.Email) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Message: "Password reset link sent", - }) -} - -// ResetPassword handles the password reset process. -// @Summary Reset user password -// @Description Resets the user's password using the provided token. -// @Accept json -// @Produce json -// @Param bodyParam body auth2.ResetPasswordRequest true "Reset password details" -// @Success 200 {object} response.BaseResponse "Password reset successful" -// @Failure 400 {object} response.BaseResponse{data=errors.Error} "Bad request" -// @Router /api/v1/auth/reset-password [post] -// @Tags Auth Password API's -func (h *AuthHandler) ResetPassword(c *gin.Context) { - ctx := auth2.GetMyContext(c) - - var req auth2.ResetPasswordChangeRequest - if err := c.ShouldBindJSON(&req); err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - if err := req.Validate(); err != nil { - response.ErrorWrapper(c, errors.NewError( - errors.ErrorBadRequest.ErrorType(), - fmt.Sprintf("invalid request %v", err.Error()))) - return - } - - err := h.service.ResetPassword(ctx, req.OldPassword, req.NewPassword) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Message: "Password reset successful", - }) -} diff --git a/internal/handlers/http/balance/balance.go b/internal/handlers/http/balance/balance.go deleted file mode 100644 index a8ab261..0000000 --- a/internal/handlers/http/balance/balance.go +++ /dev/null @@ -1,129 +0,0 @@ -package balance - -import ( - "enaklo-pos-be/internal/common/errors" - "enaklo-pos-be/internal/entity" - "enaklo-pos-be/internal/handlers/request" - "enaklo-pos-be/internal/handlers/response" - "enaklo-pos-be/internal/services" - "github.com/gin-gonic/gin" - "net/http" -) - -type Handler struct { - service services.Balance -} - -func (h *Handler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) { - route := group.Group("/balance") - - route.GET("/partner", jwt, h.GetPartnerBalance) - route.POST("/withdraw/inquiry", jwt, h.WithdrawBalanceInquiry) - route.POST("/withdraw/execute", jwt, h.WithdrawBalanceExecute) -} - -func NewHandler(service services.Balance) *Handler { - return &Handler{ - service: service, - } -} - -func (h *Handler) GetPartnerBalance(c *gin.Context) { - ctx := request.GetMyContext(c) - - if !ctx.IsPartnerAdmin() { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - updatedBranch, err := h.service.GetByID(ctx, *ctx.GetPartnerID()) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: h.toBalanceResponse(updatedBranch), - }) -} - -func (h *Handler) WithdrawBalanceInquiry(c *gin.Context) { - var req request.BalanceReq - if err := c.ShouldBindJSON(&req); err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - ctx := request.GetMyContext(c) - - if !ctx.IsPartnerAdmin() { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - inquiryResp, err := h.service.WithdrawInquiry(ctx, req.ToEntity(*ctx.GetPartnerID())) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: h.toBalanceInquiryResp(inquiryResp), - }) -} - -func (h *Handler) WithdrawBalanceExecute(c *gin.Context) { - var req request.BalanceReq - if err := c.ShouldBindJSON(&req); err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - ctx := request.GetMyContext(c) - - if !ctx.IsPartnerAdmin() { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - inquiryResp, err := h.service.WithdrawExecute(ctx, req.ToEntityReq(*ctx.GetPartnerID())) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: h.toBalanceExecuteResp(inquiryResp), - }) -} - -func (h *Handler) toBalanceResponse(resp *entity.Balance) response.Balance { - return response.Balance{ - PartnerID: resp.PartnerID, - Balance: resp.Balance, - AuthBalance: resp.AuthBalance, - } -} - -func (h *Handler) toBalanceInquiryResp(resp *entity.BalanceWithdrawInquiryResponse) response.BalanceInquiryResponse { - return response.BalanceInquiryResponse{ - PartnerID: resp.PartnerID, - Amount: resp.Amount, - Token: resp.Token, - Total: resp.Total, - Fee: resp.Fee, - } -} - -func (h *Handler) toBalanceExecuteResp(resp *entity.WalletWithdrawResponse) response.BalanceExecuteResponse { - return response.BalanceExecuteResponse{ - TransactionID: resp.TransactionID, - Status: resp.Status, - } -} diff --git a/internal/handlers/http/cashier.go b/internal/handlers/http/cashier.go deleted file mode 100644 index 2ebdf89..0000000 --- a/internal/handlers/http/cashier.go +++ /dev/null @@ -1,169 +0,0 @@ -package http - -import ( - "enaklo-pos-be/internal/common/errors" - "enaklo-pos-be/internal/handlers/request" - "enaklo-pos-be/internal/handlers/response" - "enaklo-pos-be/internal/services/v2/cashier_session" - "net/http" - "strconv" - - "github.com/gin-gonic/gin" -) - -type CashierSessionHandler struct { - service cashier_session.Service -} - -func NewCashierSession(service cashier_session.Service) *CashierSessionHandler { - return &CashierSessionHandler{ - service: service, - } -} - -func (h *CashierSessionHandler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) { - route := group.Group("/cashier-sessions") - route.Use(jwt) - - route.POST("/open", h.OpenSession) - route.POST("/close/:id", h.CloseSession) - route.GET("/open", h.GetOpenSession) - route.GET("/report/:id", h.GetSessionReport) - route.GET("/history", h.GetSessionHistory) -} - -func (h *CashierSessionHandler) OpenSession(c *gin.Context) { - ctx := request.GetMyContext(c) - - var req request.OpenCashierSessionRequest - if err := c.ShouldBindJSON(&req); err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - session, err := h.service.OpenSession(ctx, req.ToEntity(ctx.RequestedBy())) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: response.MapToCashierSessionResponse(session), - }) -} - -func (h *CashierSessionHandler) CloseSession(c *gin.Context) { - ctx := request.GetMyContext(c) - idStr := c.Param("id") - sessionID, err := strconv.ParseInt(idStr, 10, 64) - if err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - var body struct { - ClosingAmount float64 `json:"closing_amount"` - } - - if err := c.ShouldBindJSON(&body); err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - report, err := h.service.CloseSession(ctx, sessionID, body.ClosingAmount) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: response.MapToCashierSessionReport(report), - }) -} - -func (h *CashierSessionHandler) GetOpenSession(c *gin.Context) { - ctx := request.GetMyContext(c) - - cashierID := ctx.RequestedBy() - - session, err := h.service.GetOpenSession(ctx, cashierID) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: response.MapToCashierSessionResponse(session), - }) -} - -func (h *CashierSessionHandler) GetSessionReport(c *gin.Context) { - ctx := request.GetMyContext(c) - idStr := c.Param("id") - - sessionID, err := strconv.ParseInt(idStr, 10, 64) - if err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - report, err := h.service.GetSessionReport(ctx, sessionID) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: response.MapToCashierSessionReport(report), - }) -} - -func (h *CashierSessionHandler) GetSessionHistory(c *gin.Context) { - ctx := request.GetMyContext(c) - partnerID := ctx.GetPartnerID() - - // Parse query parameters - limitStr := c.DefaultQuery("limit", "10") - offsetStr := c.DefaultQuery("offset", "0") - - limit, err := strconv.Atoi(limitStr) - if err != nil || limit < 0 { - limit = 10 - } - if limit > 50 { - limit = 50 - } - - offset, err := strconv.Atoi(offsetStr) - if err != nil || offset < 0 { - offset = 0 - } - - sessions, total, err := h.service.GetSessionHistory(ctx, *partnerID, limit, offset) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - responseData := make([]*response.CashierSessionResponse, len(sessions)) - for i, session := range sessions { - responseData[i] = response.MapToCashierSessionResponse(session) - } - - pagingMeta := response.NewPaginationHelper().BuildPagingMeta(offset, limit, total) - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: responseData, - PagingMeta: pagingMeta, - }) -} diff --git a/internal/handlers/http/categories.go b/internal/handlers/http/categories.go deleted file mode 100644 index 9ffcc0d..0000000 --- a/internal/handlers/http/categories.go +++ /dev/null @@ -1,139 +0,0 @@ -package http - -import ( - "enaklo-pos-be/internal/common/errors" - "enaklo-pos-be/internal/handlers/request" - "enaklo-pos-be/internal/handlers/response" - category "enaklo-pos-be/internal/services/v2/categories" - "github.com/gin-gonic/gin" - "net/http" - "strconv" -) - -type CategoryHandler struct { - service category.Service -} - -func NewCategoryHandler(service category.Service) *CategoryHandler { - return &CategoryHandler{service: service} -} - -func (h *CategoryHandler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) { - route := group.Group("/categories") - route.Use(jwt) - - route.POST("/create", h.Create) - route.GET("/list", h.GetByPartner) - route.GET("/:id", h.GetByID) - route.PUT("/:id", h.Update) - route.DELETE("/:id", h.Delete) -} - -func (h *CategoryHandler) Create(c *gin.Context) { - ctx := request.GetMyContext(c) - - var req request.CategoryRequest - if err := c.ShouldBindJSON(&req); err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - category, err := h.service.Create(ctx, req.ToEntity(ctx.RequestedBy())) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: response.MapToCategoryResponse(category), - }) -} - -func (h *CategoryHandler) GetByPartner(c *gin.Context) { - ctx := request.GetMyContext(c) - - categories, err := h.service.GetByPartnerID(ctx, ctx.RequestedBy()) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: response.MapToCategoryListResponse(categories), - }) -} - -func (h *CategoryHandler) GetByID(c *gin.Context) { - ctx := request.GetMyContext(c) - - id, err := strconv.ParseInt(c.Param("id"), 10, 64) - if err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - category, err := h.service.GetByID(ctx, id) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: response.MapToCategoryResponse(category), - }) -} - -func (h *CategoryHandler) Update(c *gin.Context) { - ctx := request.GetMyContext(c) - - id, err := strconv.ParseInt(c.Param("id"), 10, 64) - if err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - var req request.CategoryRequest - if err := c.ShouldBindJSON(&req); err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - category := req.ToEntity(ctx.RequestedBy()) - category.ID = id - - if err := h.service.Update(ctx, category); err != nil { - response.ErrorWrapper(c, err) - return - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - }) -} - -func (h *CategoryHandler) Delete(c *gin.Context) { - ctx := request.GetMyContext(c) - - id, err := strconv.ParseInt(c.Param("id"), 10, 64) - if err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - if err := h.service.Delete(ctx, id); err != nil { - response.ErrorWrapper(c, err) - return - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - }) -} diff --git a/internal/handlers/http/customer.go b/internal/handlers/http/customer.go deleted file mode 100644 index 020bf76..0000000 --- a/internal/handlers/http/customer.go +++ /dev/null @@ -1,81 +0,0 @@ -package http - -import ( - "enaklo-pos-be/internal/services/v2/customer" - "net/http" - "strconv" - - "enaklo-pos-be/internal/common/errors" - "enaklo-pos-be/internal/entity" - "enaklo-pos-be/internal/handlers/request" - "enaklo-pos-be/internal/handlers/response" - - "github.com/gin-gonic/gin" - "github.com/go-playground/validator/v10" -) - -type CustomerHandler struct { - service customer.Service -} - -func NewCustomerHandler(service customer.Service) *CustomerHandler { - return &CustomerHandler{ - service: service, - } -} - -func (h *CustomerHandler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) { - route := group.Group("/customers") - - route.GET("/list", jwt, h.GetCustomerList) -} - -func (h *CustomerHandler) GetCustomerList(c *gin.Context) { - ctx := request.GetMyContext(c) - - searchQuery := c.DefaultQuery("search", "") - limitStr := c.DefaultQuery("limit", "10") - offsetStr := c.DefaultQuery("offset", "0") - - // Convert limit and offset to integers - limit, err := strconv.Atoi(limitStr) - if err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - offset, err := strconv.Atoi(offsetStr) - if err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - req := &entity.MemberSearch{ - Search: searchQuery, - Limit: limit, - Offset: offset, - } - - validate := validator.New() - if err := validate.Struct(req); err != nil { - response.ErrorWrapper(c, err) - return - } - - customerList, totalCount, err := h.service.GetAllCustomers(ctx, req) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: response.MapToCustomerListResponse(customerList), - PagingMeta: &response.PagingMeta{ - Page: offset + 1, - Total: int64(totalCount), - Limit: limit, - }, - }) -} diff --git a/internal/handlers/http/customer_order.go b/internal/handlers/http/customer_order.go deleted file mode 100644 index 900481f..0000000 --- a/internal/handlers/http/customer_order.go +++ /dev/null @@ -1,168 +0,0 @@ -package http - -import ( - "enaklo-pos-be/internal/common/errors" - "enaklo-pos-be/internal/entity" - "enaklo-pos-be/internal/handlers/request" - "enaklo-pos-be/internal/handlers/response" - "enaklo-pos-be/internal/services/v2/order" - "github.com/gin-gonic/gin" - "net/http" - "strconv" - "time" -) - -type CustomerOrderHandler struct { - service order.Service -} - -func NewCustomerOrderHandler(service order.Service) *CustomerOrderHandler { - return &CustomerOrderHandler{ - service: service, - } -} - -func (h *CustomerOrderHandler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) { - route := group.Group("/order") - - route.GET("/history", jwt, h.GetOrderHistory) - route.GET("/detail/:id", jwt, h.GetOrderID) -} - -func (h *CustomerOrderHandler) GetOrderHistory(c *gin.Context) { - ctx := request.GetMyContext(c) - userID := ctx.RequestedBy() - - limitStr := c.Query("limit") - offsetStr := c.Query("offset") - status := c.Query("status") - startDateStr := c.Query("start_date") - endDateStr := c.Query("end_date") - - searchReq := entity.SearchRequest{} - - if status != "" { - searchReq.Status = status - } - - limit := 10 - if limitStr != "" { - parsedLimit, err := strconv.Atoi(limitStr) - if err == nil && parsedLimit > 0 { - limit = parsedLimit - } - } - - if limit > 20 { - limit = 20 - } - - searchReq.Limit = limit - - offset := 0 - if offsetStr != "" { - parsedOffset, err := strconv.Atoi(offsetStr) - if err == nil && parsedOffset >= 0 { - offset = parsedOffset - } - } - searchReq.Offset = offset - - if startDateStr != "" { - startDate, err := time.Parse(time.RFC3339, startDateStr) - if err == nil { - searchReq.Start = startDate - } - } - - if endDateStr != "" { - endDate, err := time.Parse(time.RFC3339, endDateStr) - if err == nil { - searchReq.End = endDate - } - } - - orders, total, err := h.service.GetCustomerOrderHistory(ctx, userID, searchReq) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - responseData := []response.OrderHistoryResponse{} - for _, order := range orders { - var orderItems []response.OrderItemResponse - for _, item := range order.OrderItems { - orderItems = append(orderItems, response.OrderItemResponse{ - ProductID: item.ItemID, - ProductName: item.ItemName, - Price: item.Price, - Quantity: item.Quantity, - Subtotal: item.Price * float64(item.Quantity), - Status: item.Status, - }) - } - - responseData = append(responseData, response.OrderHistoryResponse{ - ID: order.ID, - CustomerName: order.CustomerName, - Status: order.Status, - Amount: order.Amount, - Total: order.Total, - PaymentType: h.formatPayment(order.PaymentType, order.PaymentProvider), - TableNumber: order.TableNumber, - OrderType: order.OrderType, - OrderItems: orderItems, - CreatedAt: order.CreatedAt.Format("2006-01-02T15:04:05Z"), - Tax: order.Tax, - RestaurantName: "Bakso 343", - }) - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: responseData, - PagingMeta: &response.PagingMeta{ - Page: offset + 1, - Total: int64(total), - Limit: limit, - }, - }) -} - -func (h *CustomerOrderHandler) formatPayment(payment, provider string) string { - if payment == "CASH" { - return payment - } - - return payment + " " + provider -} - -func (h *CustomerOrderHandler) GetOrderID(c *gin.Context) { - ctx := request.GetMyContext(c) - - var req GetOrderParam - if err := c.ShouldBindQuery(&req); err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - id := c.Param("id") - orderID, err := strconv.ParseInt(id, 10, 64) - if err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - order, err := h.service.GetOrderByOrderAndCustomerID(ctx, ctx.RequestedBy(), orderID) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: MapToOrderCreateResponse(order), - }) -} diff --git a/internal/handlers/http/customer_undian.go b/internal/handlers/http/customer_undian.go deleted file mode 100644 index d58c83d..0000000 --- a/internal/handlers/http/customer_undian.go +++ /dev/null @@ -1,164 +0,0 @@ -package http - -import ( - "enaklo-pos-be/internal/entity" - "enaklo-pos-be/internal/handlers/request" - "enaklo-pos-be/internal/handlers/response" - "enaklo-pos-be/internal/services/v2/undian" - "github.com/gin-gonic/gin" - "net/http" - "time" -) - -type CustomerUndianHandler struct { - undianService undian.Service -} - -func NewCustomerUndianHandler(undianService undian.Service) *CustomerUndianHandler { - return &CustomerUndianHandler{ - undianService: undianService, - } -} - -func (h *CustomerUndianHandler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) { - route := group.Group("/undian") - - route.GET("/list", jwt, h.GetUndianList) - route.GET("/events", jwt, h.GetActiveEvents) -} - -func (h *CustomerUndianHandler) GetUndianList(c *gin.Context) { - ctx := request.GetMyContext(c) - userID := ctx.RequestedBy() - - undianResponse, err := h.undianService.GetUndianList(ctx, userID) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - responseData := h.mapToUndianListResponse(undianResponse) - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: responseData, - }) -} - -func (h *CustomerUndianHandler) GetActiveEvents(c *gin.Context) { - ctx := request.GetMyContext(c) - - events, err := h.undianService.GetActiveUndianEvents(ctx) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - responseData := h.mapToActiveEventsResponse(events) - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: responseData, - }) -} - -func (h *CustomerUndianHandler) mapToUndianListResponse(undianResponse *entity.UndianListResponse) response.UndianListResponse { - events := make([]response.UndianEventResponse, 0, len(undianResponse.Events)) - - for _, event := range undianResponse.Events { - vouchers := make([]response.UndianVoucherResponse, 0, len(event.Vouchers)) - for _, voucher := range event.Vouchers { - vouchers = append(vouchers, response.UndianVoucherResponse{ - ID: voucher.ID, - VoucherCode: voucher.VoucherCode, - VoucherNumber: voucher.VoucherNumber, - IsWinner: voucher.IsWinner, - PrizeRank: voucher.PrizeRank, - WonAt: h.formatTimePointer(voucher.WonAt), - CreatedAt: voucher.CreatedAt.Format("2006-01-02T15:04:05Z"), - }) - } - - prizes := make([]response.UndianPrizeResponse, 0, len(event.Prizes)) - for _, prize := range event.Prizes { - prizes = append(prizes, response.UndianPrizeResponse{ - ID: prize.ID, - Rank: prize.Rank, - PrizeName: prize.PrizeName, - PrizeValue: prize.PrizeValue, - PrizeDescription: prize.PrizeDescription, - PrizeType: prize.PrizeType, - PrizeImageURL: prize.PrizeImageURL, - WinningVoucherID: prize.WinningVoucherID, - WinnerUserID: prize.WinnerUserID, - IsWon: prize.WinningVoucherID != nil, - Amount: prize.Amount, - }) - } - - events = append(events, response.UndianEventResponse{ - ID: event.ID, - Title: event.Title, - Description: event.Description, - ImageURL: event.ImageURL, - Status: event.Status, - StartDate: event.StartDate.Format("2006-01-02T15:04:05Z"), - EndDate: event.EndDate.Format("2006-01-02T15:04:05Z"), - DrawDate: event.DrawDate.Format("2006-01-02T15:04:05Z"), - MinimumPurchase: event.MinimumPurchase, - DrawCompleted: event.DrawCompleted, - DrawCompletedAt: h.formatTimePointer(event.DrawCompletedAt), - TermsConditions: event.TermsConditions, - Prefix: event.Prefix, - CreatedAt: event.CreatedAt.Format("2006-01-02T15:04:05Z"), - UpdatedAt: event.UpdatedAt.Format("2006-01-02T15:04:05Z"), - VoucherCount: event.VoucherCount, - Vouchers: vouchers, - Prizes: prizes, - TotalPrizes: len(prizes), - }) - } - - return response.UndianListResponse{ - Events: events, - } -} - -func (h *CustomerUndianHandler) mapToActiveEventsResponse(events []*entity.UndianEventDB) response.ActiveEventsResponse { - eventResponses := make([]response.ActiveEventResponse, 0, len(events)) - - for _, event := range events { - eventResponses = append(eventResponses, response.ActiveEventResponse{ - ID: event.ID, - Title: event.Title, - Description: event.Description, - ImageURL: event.ImageURL, - Status: event.Status, - StartDate: event.StartDate.Format("2006-01-02T15:04:05Z"), - EndDate: event.EndDate.Format("2006-01-02T15:04:05Z"), - DrawDate: event.DrawDate.Format("2006-01-02T15:04:05Z"), - MinimumPurchase: event.MinimumPurchase, - DrawCompleted: event.DrawCompleted, - DrawCompletedAt: h.formatTimePointer(event.DrawCompletedAt), - TermsConditions: event.TermsAndConditions, - Prefix: event.Prefix, - CreatedAt: event.CreatedAt.Format("2006-01-02T15:04:05Z"), - UpdatedAt: event.UpdatedAt.Format("2006-01-02T15:04:05Z"), - }) - } - - return response.ActiveEventsResponse{ - Events: eventResponses, - } -} - -// formatTimePointer formats time pointer to string -func (h *CustomerUndianHandler) formatTimePointer(t *time.Time) *string { - if t == nil { - return nil - } - formatted := t.Format("2006-01-02T15:04:05Z") - return &formatted -} diff --git a/internal/handlers/http/customerauth/auth.go b/internal/handlers/http/customerauth/auth.go deleted file mode 100644 index 5eddac7..0000000 --- a/internal/handlers/http/customerauth/auth.go +++ /dev/null @@ -1,137 +0,0 @@ -package customerauth - -import ( - "enaklo-pos-be/internal/entity" - auth2 "enaklo-pos-be/internal/handlers/request" - "enaklo-pos-be/internal/services/member" - "enaklo-pos-be/internal/services/v2/auth" - "github.com/go-playground/validator/v10" - "net/http" - "strings" - - "github.com/gin-gonic/gin" - - "enaklo-pos-be/internal/common/errors" - "enaklo-pos-be/internal/handlers/response" -) - -type AuthHandler struct { - service auth.Service - memberSvc member.RegistrationService -} - -func (a *AuthHandler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) { - authRoute := group.Group("/auth") - authRoute.POST("/login", a.AuthLogin) - authRoute.POST("/register", a.Registration) - authRoute.POST("/verify-otp", a.VerifyOTP) -} - -func NewAuthHandler(service auth.Service, memberSvc member.RegistrationService) *AuthHandler { - return &AuthHandler{ - service: service, - memberSvc: memberSvc, - } -} - -func (h *AuthHandler) AuthLogin(c *gin.Context) { - ctx := auth2.GetMyContext(c) - - var bodyParam auth2.LoginRequest - if err := c.ShouldBindJSON(&bodyParam); err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - email := strings.ToLower(bodyParam.Email) - authUser, err := h.service.AuthCustomer(ctx, email, bodyParam.Password) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - resp := response.LoginResponseCustoemr{ - ID: authUser.ID, - Token: authUser.Token, - Name: authUser.Name, - ResetPassword: authUser.ResetPassword, - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Message: "Login Success", - Data: resp, - }) -} - -func (h *AuthHandler) Registration(c *gin.Context) { - ctx := auth2.GetMyContext(c) - userID := ctx.RequestedBy() - - var req auth2.InitiateRegistrationRequest - if err := c.ShouldBindJSON(&req); err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - validate := validator.New() - if err := validate.Struct(req); err != nil { - response.ErrorWrapper(c, err) - return - } - - birthDate, err := req.GetBirthdate() - if err != nil { - response.ErrorWrapper(c, err) - return - } - - memberReq := &entity.MemberRegistrationRequest{ - Name: req.Name, - Email: req.Email, - Phone: req.Phone, - BirthDate: birthDate, - CashierID: userID, - Password: req.Password, - } - - result, err := h.memberSvc.InitiateRegistration(ctx, memberReq) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: response.MapToMemberRegistrationResponse(result), - }) -} - -func (h *AuthHandler) VerifyOTP(c *gin.Context) { - ctx := auth2.GetMyContext(c) - - var req auth2.VerifyOTPRequest - if err := c.ShouldBindJSON(&req); err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - validate := validator.New() - if err := validate.Struct(req); err != nil { - response.ErrorWrapper(c, err) - return - } - - result, err := h.memberSvc.VerifyOTP(ctx, req.Token, req.OTP) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: response.MapToMemberVerificationResponse(result.Auth), - }) -} diff --git a/internal/handlers/http/inprogress_order.go b/internal/handlers/http/inprogress_order.go deleted file mode 100644 index d4a97f8..0000000 --- a/internal/handlers/http/inprogress_order.go +++ /dev/null @@ -1,224 +0,0 @@ -package http - -import ( - "enaklo-pos-be/internal/common/errors" - "enaklo-pos-be/internal/entity" - "enaklo-pos-be/internal/handlers/request" - "enaklo-pos-be/internal/handlers/response" - "enaklo-pos-be/internal/services/v2/inprogress_order" - "github.com/gin-gonic/gin" - "github.com/go-playground/validator/v10" - "net/http" - "strconv" -) - -type InProgressOrderHandler struct { - service inprogress_order.InProgressOrderService -} - -func NewInProgressOrderHandler(service inprogress_order.InProgressOrderService) *InProgressOrderHandler { - return &InProgressOrderHandler{ - service: service, - } -} - -func (h *InProgressOrderHandler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) { - route := group.Group("/inprogress-order") - route.POST("/save", jwt, h.Save) - route.POST("/add", jwt, h.Add) - route.GET("/list", jwt, h.GetByPartnerID) -} - -type CreateInProgressOrderRequest struct { - CustomerID *int64 `json:"customer_id"` - CustomerName string `json:"customer_name" validate:"required_without=CustomerID"` - CustomerEmail string `json:"customer_email"` - CustomerPhoneNumber string `json:"customer_phone_number"` - PaymentMethod string `json:"payment_method"` - OrderItems []InProgressOrderItemRequest `json:"order_items" validate:"required,min=1,dive"` - OrderType string `json:"order_type"` - PaymentProvider string `json:"payment_provider"` - TableNumber string `json:"table_number"` - InProgressOrderID int64 `json:"in_progress_order_id"` -} - -type AddItemOrderRequest struct { - OrderItems []InProgressOrderItemRequest `json:"order_items" validate:"required,min=1,dive"` - InProgressOrderID int64 `json:"in_progress_order_id"` -} - -type InProgressOrderItemRequest struct { - ProductID int64 `json:"product_id" validate:"required"` - Quantity int `json:"quantity" validate:"required,min=1"` - Notes string `json:"notes"` -} - -type UpdateInProgressOrderRequest struct { - Status string `json:"status" validate:"required"` - Amount float64 `json:"amount" validate:"required,min=0"` - Fee float64 `json:"fee" validate:"min=0"` - Total float64 `json:"total" validate:"required,min=0"` - PaymentType string `json:"payment_type" validate:"required"` - OrderItems []InProgressOrderItemRequest `json:"order_items" validate:"required,min=1,dive"` -} - -func (h *InProgressOrderHandler) Save(c *gin.Context) { - ctx := request.GetMyContext(c) - userID := ctx.RequestedBy() - partnerID := ctx.GetPartnerID() - - var req CreateInProgressOrderRequest - if err := c.ShouldBindJSON(&req); err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - validate := validator.New() - if err := validate.Struct(req); err != nil { - response.ErrorWrapper(c, err) - return - } - - orderItems := make([]entity.OrderItemRequest, len(req.OrderItems)) - for i, item := range req.OrderItems { - orderItems[i] = entity.OrderItemRequest{ - ProductID: item.ProductID, - Quantity: item.Quantity, - Notes: item.Notes, - } - } - - order := &entity.OrderRequest{ - PartnerID: *partnerID, - CustomerID: req.CustomerID, - CustomerName: req.CustomerName, - CreatedBy: userID, - OrderItems: orderItems, - TableNumber: req.TableNumber, - OrderType: req.OrderType, - ID: req.InProgressOrderID, - Source: "POS", - } - - resp, err := h.service.Save(ctx, order) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - c.JSON(http.StatusCreated, response.BaseResponse{ - Success: true, - Status: http.StatusCreated, - Data: response.MapToOrderResponse(&entity.OrderResponse{ - Order: resp, - }), - }) -} - -func (h *InProgressOrderHandler) Add(c *gin.Context) { - ctx := request.GetMyContext(c) - - var req AddItemOrderRequest - if err := c.ShouldBindJSON(&req); err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - validate := validator.New() - if err := validate.Struct(req); err != nil { - response.ErrorWrapper(c, err) - return - } - - orderItems := make([]entity.OrderItemRequest, len(req.OrderItems)) - for i, item := range req.OrderItems { - orderItems[i] = entity.OrderItemRequest{ - ProductID: item.ProductID, - Quantity: item.Quantity, - Notes: item.Notes, - } - } - - order, err := h.service.AddItems(ctx, req.InProgressOrderID, orderItems) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - c.JSON(http.StatusCreated, response.BaseResponse{ - Success: true, - Status: http.StatusCreated, - Data: response.MapToOrderResponse(&entity.OrderResponse{ - Order: order, - }), - }) -} - -func mapToInProgressOrderResponse(order *entity.Order) map[string]interface{} { - orderItems := make([]map[string]interface{}, len(order.OrderItems)) - for i, item := range order.OrderItems { - orderItems[i] = map[string]interface{}{ - "id": item.ID, - "item_id": item.ItemID, - "quantity": item.Quantity, - "name": item.Product.Name, - "price": item.Product.Price, - "image": item.Product.Image, - } - } - - return map[string]interface{}{ - "id": order.ID, - "partner_id": order.PartnerID, - "customer_id": order.CustomerID, - "customer_name": order.CustomerName, - "payment_type": order.PaymentType, - "source": order.Source, - "created_by": order.CreatedBy, - "created_at": order.CreatedAt, - "updated_at": order.UpdatedAt, - "order_items": orderItems, - "table_number": order.TableNumber, - } -} - -func (h *InProgressOrderHandler) GetByPartnerID(c *gin.Context) { - ctx := request.GetMyContext(c) - - limitStr := c.DefaultQuery("limit", "10") - offsetStr := c.DefaultQuery("offset", "0") - - limit, err := strconv.Atoi(limitStr) - if err != nil || limit < 0 { - limit = 10 - } - - offset, err := strconv.Atoi(offsetStr) - if err != nil || offset < 0 { - offset = 0 - } - - orders, err := h.service.GetOrdersByPartnerID(ctx, *ctx.GetPartnerID(), limit, offset) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - orderResponses := make([]map[string]interface{}, len(orders)) - for i, order := range orders { - orderResponses[i] = mapToInProgressOrderResponse(order) - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: map[string]interface{}{ - "orders": orderResponses, - "pagination": map[string]interface{}{ - "limit": limit, - "offset": offset, - "count": len(orders), - }, - }, - }) -} diff --git a/internal/handlers/http/license/license.go b/internal/handlers/http/license/license.go deleted file mode 100644 index 6ab43a3..0000000 --- a/internal/handlers/http/license/license.go +++ /dev/null @@ -1,160 +0,0 @@ -package license - -import ( - "enaklo-pos-be/internal/common/errors" - "enaklo-pos-be/internal/handlers/request" - "enaklo-pos-be/internal/handlers/response" - "enaklo-pos-be/internal/middlewares" - "enaklo-pos-be/internal/services" - "github.com/gin-gonic/gin" - "github.com/go-playground/validator/v10" - "net/http" -) - -type Handler struct { - service services.License -} - -func (h *Handler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) { - route := group.Group("/license") - isAdmin := middlewares.IsAdminMiddleware() - - route.POST("/", jwt, isAdmin, h.Create) - route.GET("/", jwt, isAdmin, h.GetAll) - route.PUT("/:id", jwt, isAdmin, h.Update) - route.GET("/:id", jwt, isAdmin, h.GetByID) -} - -func NewHandler(service services.License) *Handler { - return &Handler{ - service: service, - } -} - -// Create handles the creation of a new license. -func (h *Handler) Create(c *gin.Context) { - ctx := request.GetMyContext(c) - - var req request.License - if err := c.ShouldBindJSON(&req); err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - validate := validator.New() - if err := validate.Struct(req); err != nil { - response.ErrorWrapper(c, err) - return - } - - licenseEntity, err := req.ToEntity() - if err != nil { - response.ErrorWrapper(c, err) - return - } - - res, err := h.service.Create(ctx, licenseEntity) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - var licenseResponse response.License - licenseResponse.FromEntity(res) - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: licenseResponse, - }) -} - -// Update handles the update of an existing license. -func (h *Handler) Update(c *gin.Context) { - ctx := request.GetMyContext(c) - - licenseID := c.Param("id") - - var req request.License - if err := c.ShouldBindJSON(&req); err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - validate := validator.New() - if err := validate.Struct(req); err != nil { - response.ErrorWrapper(c, err) - return - } - - licenseEntity, err := req.ToEntity() - if err != nil { - response.ErrorWrapper(c, err) - return - } - - updatedLicense, err := h.service.Update(ctx, licenseID, licenseEntity) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - var licenseResponse response.License - licenseResponse.FromEntity(updatedLicense) - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: licenseResponse, - }) -} - -// GetAll retrieves a list of licenses. -func (h *Handler) GetAll(c *gin.Context) { - var req request.LicenseParam - if err := c.ShouldBindQuery(&req); err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - ctx := request.GetMyContext(c) - - licenses, total, err := h.service.GetAll(ctx, req.Limit, req.Offset, req.Status) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - licenseResponses := response.FromEntityListAll(licenses) - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: response.LicenseList{Licenses: licenseResponses, Total: total, Limit: req.Limit, Offset: req.Offset}, - }) -} - -// GetByID retrieves details of a specific license by ID. -func (h *Handler) GetByID(c *gin.Context) { - licenseID := c.Param("id") - - res, err := h.service.GetByID(c.Request.Context(), licenseID) - if err != nil { - c.JSON(http.StatusInternalServerError, response.BaseResponse{ - Success: false, - Status: http.StatusInternalServerError, - Message: err.Error(), - Data: nil, - }) - return - } - - var licenseResponse response.License - licenseResponse.FromEntity(res) - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: licenseResponse, - }) -} diff --git a/internal/handlers/http/linqu/order.go b/internal/handlers/http/linqu/order.go deleted file mode 100644 index a9b4677..0000000 --- a/internal/handlers/http/linqu/order.go +++ /dev/null @@ -1,64 +0,0 @@ -package linkqu - -import ( - "enaklo-pos-be/internal/entity" - "enaklo-pos-be/internal/handlers/request" - "enaklo-pos-be/internal/handlers/response" - "enaklo-pos-be/internal/services" - "encoding/json" - "fmt" - "github.com/gin-gonic/gin" - "net/http" -) - -type Handler struct { - service services.Order -} - -func (h *Handler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) { - route := group.Group("/linkqu") - - route.POST("/callback", h.Callback) -} - -func NewHandler(service services.Order) *Handler { - return &Handler{ - service: service, - } -} - -func (h *Handler) Callback(c *gin.Context) { - var callbackData request.LinQuCallback - if err := c.ShouldBindJSON(&callbackData); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - data, _ := json.Marshal(callbackData) - fmt.Println(string(data)) - - err := h.service.ProcessLinkQuCallback(c, &entity.LinkQuCallback{ - PaymentReff: callbackData.PartnerReff2, - Status: callbackData.Status, - Signature: callbackData.Signature, - PartnerReff: callbackData.PartnerReff, - }) - - if err != nil { - c.JSON(http.StatusBadRequest, response.BaseResponse{ - Success: false, - Status: http.StatusBadRequest, - Message: err.Error(), - Data: nil, - }) - return - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Message: "order", - Response: "00", - }) - -} diff --git a/internal/handlers/http/member.go b/internal/handlers/http/member.go deleted file mode 100644 index 3e28477..0000000 --- a/internal/handlers/http/member.go +++ /dev/null @@ -1,154 +0,0 @@ -package http - -import ( - "enaklo-pos-be/internal/common/errors" - "enaklo-pos-be/internal/entity" - "enaklo-pos-be/internal/handlers/request" - "enaklo-pos-be/internal/handlers/response" - "enaklo-pos-be/internal/services/member" - "github.com/gin-gonic/gin" - "github.com/go-playground/validator/v10" - "net/http" -) - -type MemberHandler struct { - service member.RegistrationService -} - -func NewMemberRegistrationHandler(service member.RegistrationService) *MemberHandler { - return &MemberHandler{ - service: service, - } -} - -func (h *MemberHandler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) { - route := group.Group("/member") - - route.POST("/register", jwt, h.InitiateRegistration) - route.POST("/verify", jwt, h.VerifyOTP) - route.GET("/status", jwt, h.GetRegistrationStatus) - route.POST("/resend-otp", jwt, h.ResendOTP) - route.GET("/list", jwt, h.GetRegistrationStatus) -} - -func (h *MemberHandler) InitiateRegistration(c *gin.Context) { - ctx := request.GetMyContext(c) - userID := ctx.RequestedBy() - - var req request.InitiateRegistrationRequest - if err := c.ShouldBindJSON(&req); err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - validate := validator.New() - if err := validate.Struct(req); err != nil { - response.ErrorWrapper(c, err) - return - } - - birthDate, err := req.GetBirthdate() - if err != nil { - response.ErrorWrapper(c, err) - return - } - - memberReq := &entity.MemberRegistrationRequest{ - Name: req.Name, - Email: req.Email, - Phone: req.Phone, - BirthDate: birthDate, - BranchID: *ctx.GetPartnerID(), - CashierID: userID, - } - - result, err := h.service.InitiateRegistration(ctx, memberReq) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: response.MapToMemberRegistrationResponse(result), - }) -} - -func (h *MemberHandler) VerifyOTP(c *gin.Context) { - ctx := request.GetMyContext(c) - - var req request.VerifyOTPRequest - if err := c.ShouldBindJSON(&req); err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - validate := validator.New() - if err := validate.Struct(req); err != nil { - response.ErrorWrapper(c, err) - return - } - - result, err := h.service.VerifyOTP(ctx, req.Token, req.OTP) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: response.MapToMemberVerificationResponse(result.Auth), - }) -} - -func (h *MemberHandler) GetRegistrationStatus(c *gin.Context) { - ctx := request.GetMyContext(c) - token := c.Query("token") - - if token == "" { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - result, err := h.service.GetRegistrationStatus(ctx, token) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: response.MapToMemberRegistrationStatus(result), - }) -} - -func (h *MemberHandler) ResendOTP(c *gin.Context) { - ctx := request.GetMyContext(c) - - var req entity.ResendOTPRequest - if err := c.ShouldBindJSON(&req); err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - validate := validator.New() - if err := validate.Struct(req); err != nil { - response.ErrorWrapper(c, err) - return - } - - result, err := h.service.ResendOTP(ctx, req.Token) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: response.MapToResendOTPResponse(result), - }) -} diff --git a/internal/handlers/http/menu.go b/internal/handlers/http/menu.go deleted file mode 100644 index b9cf4a0..0000000 --- a/internal/handlers/http/menu.go +++ /dev/null @@ -1,213 +0,0 @@ -package http - -import ( - "enaklo-pos-be/internal/common/errors" - "enaklo-pos-be/internal/entity" - "enaklo-pos-be/internal/handlers/request" - "enaklo-pos-be/internal/handlers/response" - "enaklo-pos-be/internal/services/v2/inprogress_order" - "enaklo-pos-be/internal/services/v2/product" - "github.com/gin-gonic/gin" - "github.com/go-playground/validator/v10" - "net/http" - "strconv" -) - -type MenuHandler struct { - service product.Service - orderService inprogress_order.InProgressOrderService -} - -func NewMenuHandler(service product.Service, orderService inprogress_order.InProgressOrderService, -) *MenuHandler { - return &MenuHandler{ - service: service, - orderService: orderService, - } -} - -func (h *MenuHandler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) { - route := group.Group("/menu") - - route.GET("/:partner_id", h.GetProducts) - route.POST("/order/create", h.OrderCreate) - route.POST("/order/member/create", jwt, h.OrderMemberCreate) - route.GET("/order", h.GetOrderID) -} - -func (h *MenuHandler) GetProducts(c *gin.Context) { - ctx := request.GetMyContext(c) - - partnerIDParam := c.Param("partner_id") - partnerID, err := strconv.ParseInt(partnerIDParam, 10, 64) - if err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - if partnerID <= 0 { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - var req request.ProductParam - if err := c.ShouldBindQuery(&req); err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - searchParam := req.ToEntity(partnerID) - searchParam.PartnerID = partnerID - - products, total, err := h.service.GetProductsByPartnerID(ctx, searchParam) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: h.toProductResponseList(products, int64(total), req), - }) -} - -func (h *MenuHandler) toProductResponseList(resp []*entity.Product, total int64, req request.ProductParam) response.ProductList { - var products []response.Product - for _, b := range resp { - products = append(products, h.toProductResponse(b)) - } - - return response.ProductList{ - Products: products, - Total: total, - Limit: req.Limit, - Offset: req.Offset, - } -} - -func (h *MenuHandler) toProductResponse(resp *entity.Product) response.Product { - return response.Product{ - ID: resp.ID, - Name: resp.Name, - Type: resp.Type, - Price: resp.Price, - Status: resp.Status, - Description: resp.Description, - Image: resp.Image, - } -} - -func (h *MenuHandler) OrderCreate(c *gin.Context) { - ctx := request.GetMyContext(c) - - var req request.OrderCustomer - if err := c.ShouldBindJSON(&req); err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - validate := validator.New() - if err := validate.Struct(req); err != nil { - response.ErrorWrapper(c, err) - return - } - - if req.PartnerID == 0 { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - orderRequest := req.ToEntity(req.PartnerID, 0) - - order, err := h.orderService.Save(ctx, orderRequest) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: MapToOrderCreateResponse(order), - }) -} - -func (h *MenuHandler) OrderMemberCreate(c *gin.Context) { - ctx := request.GetMyContext(c) - - var req request.OrderCustomer - if err := c.ShouldBindJSON(&req); err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - validate := validator.New() - if err := validate.Struct(req); err != nil { - response.ErrorWrapper(c, err) - return - } - - if req.PartnerID == 0 { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - userID := ctx.RequestedBy() - orderRequest := req.ToEntity(req.PartnerID, userID) - orderRequest.CustomerID = &userID - - order, err := h.orderService.Save(ctx, orderRequest) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: MapToOrderCreateResponse(order), - }) -} - -type GetOrderParam struct { - PartnerID int64 `form:"partner_id" json:"partner_id"` - OrderID int64 `form:"order_id" json:"order_id"` -} - -func (h *MenuHandler) GetOrderID(c *gin.Context) { - ctx := request.GetMyContext(c) - - var req GetOrderParam - if err := c.ShouldBindQuery(&req); err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - order, err := h.orderService.GetOrderByOrderAndPartnerID(ctx, req.PartnerID, req.OrderID) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: MapToOrderCreateResponse(order), - }) -} - -func MapToOrderCreateResponse(result *entity.Order) response.OrderResponse { - resp := response.OrderResponse{ - ID: result.ID, - Status: result.Status, - Amount: result.Amount, - Tax: result.Tax, - Total: result.Total, - PaymentType: result.PaymentType, - CreatedAt: result.CreatedAt, - Items: response.MapToOrderItemResponses(result.OrderItems), - } - - return resp -} diff --git a/internal/handlers/http/midtrans/order.go b/internal/handlers/http/midtrans/order.go deleted file mode 100644 index 6f77aa6..0000000 --- a/internal/handlers/http/midtrans/order.go +++ /dev/null @@ -1,71 +0,0 @@ -package mdtrns - -import ( - "enaklo-pos-be/internal/handlers/request" - "enaklo-pos-be/internal/handlers/response" - "enaklo-pos-be/internal/services" - "github.com/gin-gonic/gin" - "net/http" -) - -type Handler struct { - service services.Order -} - -func (h *Handler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) { - route := group.Group("/midtrans") - - route.POST("/callback", h.Callback) -} - -func NewHandler(service services.Order) *Handler { - return &Handler{ - service: service, - } -} - -func (h *Handler) Callback(c *gin.Context) { - var callbackData request.MidtransCallbackRequest - if err := c.ShouldBindJSON(&callbackData); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - validStatuses := []string{"settlement", "expire", "deny", "cancel", "capture", "failure"} - - isValidStatus := false - for _, status := range validStatuses { - if callbackData.TransactionStatus == status { - isValidStatus = true - break - } - } - - if !isValidStatus { - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Message: "", - }) - return - } - - err := h.service.ProcessCallback(c, callbackData.ToEntity()) - - if err != nil { - c.JSON(http.StatusUnauthorized, response.BaseResponse{ - Success: false, - Status: http.StatusBadRequest, - Message: err.Error(), - Data: nil, - }) - return - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Message: "order", - }) - -} diff --git a/internal/handlers/http/order.go b/internal/handlers/http/order.go deleted file mode 100644 index aaa38b2..0000000 --- a/internal/handlers/http/order.go +++ /dev/null @@ -1,855 +0,0 @@ -package http - -import ( - "enaklo-pos-be/internal/common/errors" - order2 "enaklo-pos-be/internal/constants/order" - "enaklo-pos-be/internal/entity" - "enaklo-pos-be/internal/handlers/request" - "enaklo-pos-be/internal/handlers/response" - "enaklo-pos-be/internal/services/v2/order" - "net/http" - "strconv" - "time" - - "github.com/gin-gonic/gin" - "github.com/go-playground/validator/v10" -) - -type Handler struct { - service order.Service - queryParser *request.QueryParser -} - -func NewOrderHandler(service order.Service) *Handler { - return &Handler{ - service: service, - queryParser: request.NewQueryParser(), - } -} - -func (h *Handler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) { - route := group.Group("/order") - - route.POST("/inquiry", jwt, h.Inquiry) - route.POST("/execute", jwt, h.Execute) - route.POST("/refund", jwt, h.Refund) - route.POST("/partial-refund", jwt, h.PartialRefund) - route.POST("/void", jwt, h.VoidOrder) - route.POST("/split-bill", jwt, h.SplitBill) - route.GET("/history", jwt, h.GetOrderHistory) - route.GET("/refund-history", jwt, h.GetRefundHistory) - route.GET("/payment-analysis", jwt, h.GetPaymentMethodAnalysis) - route.GET("/revenue-overview", jwt, h.GetRevenueOverview) - route.GET("/sales-by-category", jwt, h.GetSalesByCategory) - route.GET("/popular-products", jwt, h.GetPopularProducts) - route.GET("/detail/:id", jwt, h.GetByID) -} - -type InquiryRequest struct { - CustomerID *int64 `json:"customer_id"` - CustomerName string `json:"customer_name" validate:"required_without=CustomerID"` - CustomerEmail string `json:"customer_email"` - CustomerPhoneNumber string `json:"customer_phone_number"` - PaymentMethod string `json:"payment_method" validate:"required"` - OrderItems []OrderItemRequest `json:"order_items" validate:"required,min=1,dive"` - OrderType string `json:"order_type"` - PaymentProvider string `json:"payment_provider"` - TableNumber string `json:"table_number"` - CashierSessionID int64 `json:"cashier_session_id"` -} - -func (o *InquiryRequest) GetPaymentProvider() string { - if o.PaymentMethod == "CASH" { - return "CASH" - } - - return o.PaymentProvider -} - -type OrderItemRequest struct { - ProductID int64 `json:"product_id" validate:"required"` - Quantity int `json:"quantity"` - Notes string `json:"notes"` -} - -type ExecuteRequest struct { - PaymentMethod string `json:"payment_method" validate:"required"` - PaymentProvider string `json:"payment_provider"` - InProgressOrderID int64 `json:"in_progress_order_id"` - Token string `json:"token"` -} - -type RefundRequest struct { - OrderID int64 `json:"order_id" validate:"required"` - Reason string `json:"reason" validate:"required"` -} - -type PartialRefundRequest struct { - OrderID int64 `json:"order_id" validate:"required"` - Reason string `json:"reason" validate:"required"` - Items []PartialRefundItemRequest `json:"items" validate:"required,min=1,dive"` -} - -type PartialRefundItemRequest struct { - OrderItemID int64 `json:"order_item_id" validate:"required"` - Quantity int `json:"quantity" validate:"required,min=1"` -} - -type VoidOrderRequest struct { - OrderID int64 `json:"order_id" validate:"required"` - Reason string `json:"reason" validate:"required"` - Type string `json:"type" validate:"required,oneof=ALL ITEM"` - Items []VoidItemRequest `json:"items,omitempty" validate:"required_if=Type ITEM,dive"` -} - -type VoidItemRequest struct { - OrderItemID int64 `json:"order_item_id" validate:"required"` - Quantity int `json:"quantity" validate:"required,min=1"` -} - -type SplitBillRequest struct { - OrderID int64 `json:"order_id" validate:"required"` - Type string `json:"type" validate:"required,oneof=ITEM AMOUNT"` - Items []SplitBillItemRequest `json:"items,omitempty" validate:"required_if=Type ITEM,dive"` - Amount float64 `json:"amount,omitempty" validate:"required_if=Type AMOUNT,min=0"` -} - -type SplitBillItemRequest struct { - OrderItemID int64 `json:"order_item_id" validate:"required"` - Quantity int `json:"quantity" validate:"required,min=1"` -} - -type RefundResponse struct { - OrderID int64 `json:"order_id"` - Status string `json:"status"` - RefundAmount float64 `json:"refund_amount"` - Reason string `json:"reason"` - RefundedAt string `json:"refunded_at"` - CustomerName string `json:"customer_name"` - PaymentType string `json:"payment_type"` -} - -type RefundHistoryResponse struct { - OrderID int64 `json:"order_id"` - CustomerName string `json:"customer_name"` - CustomerID *int64 `json:"customer_id"` - IsMember bool `json:"is_member"` - Status string `json:"status"` - Amount float64 `json:"amount"` - Total float64 `json:"total"` - PaymentType string `json:"payment_type"` - TableNumber string `json:"table_number"` - OrderType string `json:"order_type"` - CreatedAt string `json:"created_at"` - RefundedAt string `json:"refunded_at"` - Tax float64 `json:"tax"` -} - -type PartialRefundResponse struct { - OrderID int64 `json:"order_id"` - Status string `json:"status"` - RefundedAmount float64 `json:"refunded_amount"` - RemainingAmount float64 `json:"remaining_amount"` - Reason string `json:"reason"` - RefundedAt string `json:"refunded_at"` - CustomerName string `json:"customer_name"` - PaymentType string `json:"payment_type"` - RefundedItems []RefundedItemResponse `json:"refunded_items"` -} - -type RefundedItemResponse struct { - OrderItemID int64 `json:"order_item_id"` - ItemName string `json:"item_name"` - Quantity int `json:"quantity"` - UnitPrice float64 `json:"unit_price"` - TotalPrice float64 `json:"total_price"` -} - -type VoidOrderResponse struct { - OrderID int64 `json:"order_id"` - Status string `json:"status"` - Reason string `json:"reason"` - VoidedAt string `json:"voided_at"` - CustomerName string `json:"customer_name"` - VoidedItems []VoidedItemResponse `json:"voided_items,omitempty"` -} - -type VoidedItemResponse struct { - OrderItemID int64 `json:"order_item_id"` - ItemName string `json:"item_name"` - Quantity int `json:"quantity"` - UnitPrice float64 `json:"unit_price"` - TotalPrice float64 `json:"total_price"` -} - -type SplitBillResponse struct { - OriginalOrderID int64 `json:"original_order_id"` - SplitOrders []SplitOrderResponse `json:"split_orders"` - SplitAt string `json:"split_at"` -} - -type SplitOrderResponse struct { - OrderID int64 `json:"order_id"` - CustomerName string `json:"customer_name"` - CustomerID *int64 `json:"customer_id"` - Amount float64 `json:"amount"` - Total float64 `json:"total"` - Tax float64 `json:"tax"` - Status string `json:"status"` - Items []response.OrderItemResponse `json:"items"` -} - -func (h *Handler) Inquiry(c *gin.Context) { - ctx := request.GetMyContext(c) - userID := ctx.RequestedBy() - partnerID := ctx.GetPartnerID() - - var req InquiryRequest - if err := c.ShouldBindJSON(&req); err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - validate := validator.New() - if err := validate.Struct(req); err != nil { - response.ErrorWrapper(c, err) - return - } - - orderItems := make([]entity.OrderItemRequest, len(req.OrderItems)) - for i, item := range req.OrderItems { - orderItems[i] = entity.OrderItemRequest{ - ProductID: item.ProductID, - Quantity: item.Quantity, - Notes: item.Notes, - } - } - - orderReq := &entity.OrderRequest{ - Source: "POS", - CreatedBy: userID, - PartnerID: *partnerID, - PaymentMethod: req.PaymentMethod, - OrderItems: orderItems, - CustomerID: req.CustomerID, - CustomerName: req.CustomerName, - CustomerEmail: req.CustomerEmail, - CustomerPhoneNumber: req.CustomerPhoneNumber, - OrderType: req.OrderType, - PaymentProvider: req.GetPaymentProvider(), - TableNumber: req.TableNumber, - CashierSessionID: req.CashierSessionID, - } - - result, err := h.service.CreateOrderInquiry(ctx, orderReq) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: response.MapToInquiryResponse(result), - }) -} - -func (h *Handler) Execute(c *gin.Context) { - ctx := request.GetMyContext(c) - - var req ExecuteRequest - if err := c.ShouldBindJSON(&req); err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - validate := validator.New() - if err := validate.Struct(req); err != nil { - response.ErrorWrapper(c, err) - return - } - - result, err := h.service.ExecuteOrderInquiry(ctx, req.Token, req.PaymentMethod, req.PaymentProvider, req.InProgressOrderID) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: response.MapToOrderResponse(result), - }) -} - -func (h *Handler) Refund(c *gin.Context) { - ctx := request.GetMyContext(c) - - var req RefundRequest - if err := c.ShouldBindJSON(&req); err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - validate := validator.New() - if err := validate.Struct(req); err != nil { - response.ErrorWrapper(c, err) - return - } - - err := h.service.RefundRequest(ctx, *ctx.GetPartnerID(), req.OrderID, req.Reason) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - order, err := h.service.GetOrderByID(ctx, req.OrderID) - if err != nil { - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Message: "Refund processed successfully", - }) - return - } - - refundResponse := RefundResponse{ - OrderID: order.ID, - Status: order.Status, - RefundAmount: order.Total, - Reason: req.Reason, - RefundedAt: order.UpdatedAt.Format("2006-01-02T15:04:05Z"), - CustomerName: order.CustomerName, - PaymentType: response.NewPaymentFormatter().Format(order.PaymentType, order.PaymentProvider), - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: refundResponse, - }) -} - -func (h *Handler) GetOrderHistory(c *gin.Context) { - ctx := request.GetMyContext(c) - - searchReq, err := h.queryParser.ParseSearchRequest(c) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - orders, total, err := h.service.GetOrderHistory(ctx, *searchReq) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - responseData := response.MapOrderHistoryResponse(orders) - pagingMeta := response.NewPaginationHelper().BuildPagingMeta(searchReq.Offset, searchReq.Limit, total) - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: responseData, - PagingMeta: pagingMeta, - }) -} - -func (h *Handler) GetPaymentMethodAnalysis(c *gin.Context) { - ctx := request.GetMyContext(c) - partnerID := ctx.GetPartnerID() - - // Parse query parameters - limitStr := c.Query("limit") - offsetStr := c.Query("offset") - status := c.Query("status") - startDateStr := c.Query("start_date") - endDateStr := c.Query("end_date") - - searchReq := entity.SearchRequest{} - - limit := 10 - if limitStr != "" { - parsedLimit, err := strconv.Atoi(limitStr) - if err == nil && parsedLimit > 0 { - limit = parsedLimit - } - } - if limit > 20 { - limit = 20 - } - searchReq.Limit = limit - - offset := 0 - if offsetStr != "" { - parsedOffset, err := strconv.Atoi(offsetStr) - if err == nil && parsedOffset >= 0 { - offset = parsedOffset - } - } - searchReq.Offset = offset - - if status != "" { - searchReq.Status = status - } - - if startDateStr != "" { - startDate, err := time.Parse(time.RFC3339, startDateStr) - if err == nil { - searchReq.Start = startDate - } - } - - if endDateStr != "" { - endDate, err := time.Parse(time.RFC3339, endDateStr) - if err == nil { - searchReq.End = endDate - } - } - - paymentAnalysis, err := h.service.GetOrderPaymentAnalysis(ctx, *partnerID, searchReq) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - paymentBreakdown := make([]PaymentMethodBreakdown, len(paymentAnalysis.PaymentMethodBreakdown)) - for i, bd := range paymentAnalysis.PaymentMethodBreakdown { - paymentBreakdown[i] = PaymentMethodBreakdown{ - PaymentMethod: response.NewPaymentFormatter().Format(bd.PaymentType, bd.PaymentProvider), - TotalTransactions: bd.TotalTransactions, - TotalAmount: bd.TotalAmount, - } - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: PaymentMethodAnalysisResponse{ - PaymentMethodBreakdown: paymentBreakdown, - TotalAmount: paymentAnalysis.TotalAmount, - TotalTransactions: paymentAnalysis.TotalTransactions, - }, - }) -} - -type PaymentMethodBreakdown struct { - PaymentMethod string `json:"payment_method"` - TotalTransactions int64 `json:"total_transactions"` - TotalAmount float64 `json:"total_amount"` - AverageTransactionAmount float64 `json:"average_transaction_amount"` - Percentage float64 `json:"percentage"` -} - -type PaymentMethodAnalysisResponse struct { - PaymentMethodBreakdown []PaymentMethodBreakdown `json:"payment_method_breakdown"` - TotalAmount float64 `json:"total_amount"` - TotalTransactions int64 `json:"total_transactions"` - - MostUsedPaymentMethod string `json:"most_used_payment_method"` - HighestRevenueMethod string `json:"highest_revenue_method"` -} - -func (h *Handler) GetRevenueOverview(c *gin.Context) { - ctx := request.GetMyContext(c) - partnerID := ctx.GetPartnerID() - - granularity := c.Query("period") - - year := time.Now().Year() - - if granularity != "m" && granularity != "w" && granularity != "d" { - granularity = "m" - } - - revenueOverview, err := h.service.GetRevenueOverview( - ctx, - *partnerID, - year, - granularity, - order2.Paid.String(), - ) - - if err != nil { - response.ErrorWrapper(c, err) - return - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: revenueOverview, - }) -} - -func (h *Handler) GetSalesByCategory(c *gin.Context) { - ctx := request.GetMyContext(c) - partnerID := ctx.GetPartnerID() - - period := c.Query("period") - status := order2.Paid.String() - - if period != "d" && period != "w" && period != "m" { - period = "d" - } - - salesByCategory, err := h.service.GetSalesByCategory( - ctx, - *partnerID, - period, - status, - ) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: salesByCategory, - }) -} - -func (h *Handler) GetPopularProducts(c *gin.Context) { - ctx := request.GetMyContext(c) - partnerID := ctx.GetPartnerID() - - period := c.Query("period") - status := order2.Paid.String() - sortBy := c.Query("sort_by") - - limit := 1000 - - if period != "d" && period != "w" && period != "m" { - period = "d" - } - - popularProducts, err := h.service.GetPopularProducts( - ctx, - *partnerID, - period, - status, - limit, - sortBy, - ) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: popularProducts, - }) -} - -func (h *Handler) GetRefundHistory(c *gin.Context) { - ctx := request.GetMyContext(c) - - limitStr := c.Query("limit") - offsetStr := c.Query("offset") - startDateStr := c.Query("start_date") - endDateStr := c.Query("end_date") - - searchReq := entity.SearchRequest{} - - limit := 20 - if limitStr != "" { - parsedLimit, err := strconv.Atoi(limitStr) - if err == nil && parsedLimit > 0 { - limit = parsedLimit - } - } - - if limit > 100 { - limit = 100 - } - - searchReq.Limit = limit - - offset := 0 - if offsetStr != "" { - parsedOffset, err := strconv.Atoi(offsetStr) - if err == nil && parsedOffset >= 0 { - offset = parsedOffset - } - } - - searchReq.Offset = offset - searchReq.Status = "REFUNDED" - - if startDateStr != "" { - startDate, err := time.Parse(time.RFC3339, startDateStr) - if err == nil { - searchReq.Start = startDate - } - } - - if endDateStr != "" { - endDate, err := time.Parse(time.RFC3339, endDateStr) - if err == nil { - searchReq.End = endDate - } - } - - orders, total, err := h.service.GetOrderHistory(ctx, searchReq) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - responseData := []RefundHistoryResponse{} - for _, order := range orders { - responseData = append(responseData, RefundHistoryResponse{ - OrderID: order.ID, - CustomerName: order.CustomerName, - CustomerID: order.CustomerID, - IsMember: order.IsMemberOrder(), - Status: order.Status, - Amount: order.Amount, - Total: order.Total, - PaymentType: response.NewPaymentFormatter().Format(order.PaymentType, order.PaymentProvider), - TableNumber: order.TableNumber, - OrderType: order.OrderType, - CreatedAt: order.CreatedAt.Format("2006-01-02T15:04:05Z"), - RefundedAt: order.UpdatedAt.Format("2006-01-02T15:04:05Z"), - Tax: order.Tax, - }) - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: responseData, - PagingMeta: &response.PagingMeta{ - Page: offset + 1, - Total: int64(total), - Limit: limit, - }, - }) -} - -func (h *Handler) PartialRefund(c *gin.Context) { - ctx := request.GetMyContext(c) - - var req PartialRefundRequest - if err := c.ShouldBindJSON(&req); err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - validate := validator.New() - if err := validate.Struct(req); err != nil { - response.ErrorWrapper(c, err) - return - } - - items := make([]entity.PartialRefundItem, len(req.Items)) - for i, item := range req.Items { - items[i] = entity.PartialRefundItem{ - OrderItemID: item.OrderItemID, - Quantity: item.Quantity, - } - } - - err := h.service.PartialRefundRequest(ctx, *ctx.GetPartnerID(), req.OrderID, req.Reason, items) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - // Get updated order to return details - order, err := h.service.GetOrderByID(ctx, req.OrderID) - if err != nil { - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Message: "Partial refund processed successfully", - }) - return - } - - refundedAmount := 0.0 - var refundedItems []RefundedItemResponse - - for _, reqItem := range req.Items { - for _, orderItem := range order.OrderItems { - if orderItem.ID == reqItem.OrderItemID { - itemTotal := orderItem.Price * float64(reqItem.Quantity) - refundedAmount += itemTotal - - refundedItems = append(refundedItems, RefundedItemResponse{ - OrderItemID: orderItem.ID, - ItemName: orderItem.ItemName, - Quantity: reqItem.Quantity, - UnitPrice: orderItem.Price, - TotalPrice: itemTotal, - }) - break - } - } - } - - partialRefundResponse := PartialRefundResponse{ - OrderID: order.ID, - Status: order.Status, - RefundedAmount: refundedAmount, - RemainingAmount: order.Total, - Reason: req.Reason, - RefundedAt: order.UpdatedAt.Format("2006-01-02T15:04:05Z"), - CustomerName: order.CustomerName, - PaymentType: response.NewPaymentFormatter().Format(order.PaymentType, order.PaymentProvider), - RefundedItems: refundedItems, - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: partialRefundResponse, - }) -} - -func (h *Handler) VoidOrder(c *gin.Context) { - ctx := request.GetMyContext(c) - - var req VoidOrderRequest - if err := c.ShouldBindJSON(&req); err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - validate := validator.New() - if err := validate.Struct(req); err != nil { - response.ErrorWrapper(c, err) - return - } - - var items []entity.VoidItem - if req.Type == "ITEM" { - items = make([]entity.VoidItem, len(req.Items)) - for i, item := range req.Items { - items[i] = entity.VoidItem{ - OrderItemID: item.OrderItemID, - Quantity: item.Quantity, - } - } - } - - err := h.service.VoidOrderRequest(ctx, *ctx.GetPartnerID(), req.OrderID, req.Reason, req.Type, items) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - // Get updated order to return details - order, err := h.service.GetOrderByID(ctx, req.OrderID) - if err != nil { - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Message: "Order voided successfully", - }) - return - } - - var voidedItems []VoidedItemResponse - if req.Type == "ITEM" { - for _, reqItem := range req.Items { - for _, orderItem := range order.OrderItems { - if orderItem.ID == reqItem.OrderItemID { - itemTotal := orderItem.Price * float64(reqItem.Quantity) - - voidedItems = append(voidedItems, VoidedItemResponse{ - OrderItemID: orderItem.ID, - ItemName: orderItem.ItemName, - Quantity: reqItem.Quantity, - UnitPrice: orderItem.Price, - TotalPrice: itemTotal, - }) - break - } - } - } - } - - voidOrderResponse := VoidOrderResponse{ - OrderID: order.ID, - Status: order.Status, - Reason: req.Reason, - VoidedAt: order.UpdatedAt.Format("2006-01-02T15:04:05Z"), - CustomerName: order.CustomerName, - VoidedItems: voidedItems, - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: voidOrderResponse, - }) -} - -func (h *Handler) SplitBill(c *gin.Context) { - ctx := request.GetMyContext(c) - - var req SplitBillRequest - if err := c.ShouldBindJSON(&req); err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - validate := validator.New() - if err := validate.Struct(req); err != nil { - response.ErrorWrapper(c, err) - return - } - - var items []entity.SplitBillItem - if req.Type == "ITEM" { - items = make([]entity.SplitBillItem, len(req.Items)) - for i, item := range req.Items { - items[i] = entity.SplitBillItem{ - OrderItemID: item.OrderItemID, - Quantity: item.Quantity, - } - } - } - - splitOrder, err := h.service.SplitBillRequest(ctx, - *ctx.GetPartnerID(), req.OrderID, req.Type, items, req.Amount) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: response.MapToOrderResponse(&entity.OrderResponse{Order: splitOrder}), - }) -} - -func (h *Handler) GetByID(c *gin.Context) { - ctx := request.GetMyContext(c) - partnerID := ctx.GetPartnerID() - - orderID, err := strconv.ParseInt(c.Param("id"), 10, 64) - if err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - order, err := h.service.GetOrderByIDAndPartnerID(ctx, orderID, *partnerID) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: response.MapToOrderResponse(&entity.OrderResponse{Order: order}), - }) -} diff --git a/internal/handlers/http/oss/oss.go b/internal/handlers/http/oss/oss.go deleted file mode 100644 index d6445a1..0000000 --- a/internal/handlers/http/oss/oss.go +++ /dev/null @@ -1,92 +0,0 @@ -package oss - -import ( - "enaklo-pos-be/internal/entity" - "enaklo-pos-be/internal/handlers/response" - "enaklo-pos-be/internal/services" - "fmt" - "mime/multipart" - "net/http" - - "github.com/gin-gonic/gin" -) - -const _oneMB = 1 << 20 // 1MB -const _maxUploadSizeMB = 2 * _oneMB -const _folderName = "/public" - -type OssHandler struct { - ossService services.OSSService -} - -func (h *OssHandler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) { - route := group.Group("/file") - - route.POST("/upload", jwt, h.UploadFile) -} - -func NewOssHandler(ossService services.OSSService) *OssHandler { - return &OssHandler{ - ossService: ossService, - } -} - -// UploadFile handles the uploading of a file to OSS. -// @Summary Upload a file to OSS -// @Description Upload a file to Alibaba Cloud OSS with the provided details. -// @Accept mpfd -// @Produce json -// @Param Authorization header string true "JWT token" -// @Param file formData file true "File to upload (max size: 2MB)" -// @Success 200 {object} response.BaseResponse{data=entity.UploadFileResponse} "File uploaded successfully" -// @Failure 400 {object} response.BaseResponse{data=errors.Error} "Bad request" -// @Failure 401 {object} response.BaseResponse{data=errors.Error} "Unauthorized" -// @Tags File Upload API -// @Router /api/v1/file/upload [post] -func (h *OssHandler) UploadFile(c *gin.Context) { - // Get the oss file from the request form - file, err := c.FormFile("file") - if err != nil { - c.JSON(http.StatusBadRequest, response.BaseResponse{ - ErrorMessage: "Failed to retrieve the file", - }) - return - } - - // Check if the uploaded file is an image (photo) - //if !isPDFFile(file) { - // c.JSON(http.StatusBadRequest, response.BaseResponse{ - // ErrorMessage: "Only image files are allowed", - // }) - // return - //} - - // Check if the file size is not greater than the maximum allowed size - if file.Size > _maxUploadSizeMB { - c.JSON(http.StatusBadRequest, response.BaseResponse{ - ErrorMessage: fmt.Sprintf("The file is too big. The maximum size is %d", _maxUploadSizeMB/_oneMB), - }) - return - } - - // Call the service to oss the file to Alibaba Cloud OSS - ret, err := h.ossService.UploadFile(c, &entity.UploadFileRequest{ - FileHeader: file, - FolderName: _folderName, - }) - - if err != nil { - response.ErrorWrapper(c, err) - return - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Data: ret, - Success: true, - }) -} - -func isPDFFile(file *multipart.FileHeader) bool { - contentType := file.Header.Get("Content-Type") - return contentType == "application/pdf" -} diff --git a/internal/handlers/http/partner/partner.go b/internal/handlers/http/partner/partner.go deleted file mode 100644 index 002e9a5..0000000 --- a/internal/handlers/http/partner/partner.go +++ /dev/null @@ -1,295 +0,0 @@ -package partner - -import ( - "enaklo-pos-be/internal/common/errors" - "enaklo-pos-be/internal/entity" - "enaklo-pos-be/internal/handlers/request" - "enaklo-pos-be/internal/handlers/response" - "enaklo-pos-be/internal/middlewares" - "enaklo-pos-be/internal/services" - "net/http" - "strconv" - "time" - - "github.com/gin-gonic/gin" - "github.com/go-playground/validator/v10" -) - -type Handler struct { - service services.Partner -} - -func (h *Handler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) { - route := group.Group("/partner") - isSuperAdmin := middlewares.SuperAdminMiddleware() - - route.POST("/", jwt, isSuperAdmin, h.Create) - route.GET("/list", jwt, h.GetAll) - route.PUT("/:id", jwt, isSuperAdmin, h.Update) - route.GET("/:id", jwt, isSuperAdmin, h.GetByID) - route.DELETE("/:id", jwt, isSuperAdmin, h.Delete) - route.PUT("/update", jwt, h.UpdateMyStore) -} - -func NewHandler(service services.Partner) *Handler { - return &Handler{ - service: service, - } -} - -// Create handles the creation of a new Partner. -// @Summary Create a new Partner -// @Description Create a new Partner based on the provided data. -// @Accept json -// @Produce json -// @Param Authorization header string true "JWT token" -// @Param req body request.Partner true "New Partner details" -// @Success 200 {object} response.BaseResponse{data=response.Partner} "Partner created successfully" -// @Failure 400 {object} response.BaseResponse{data=errors.Error} "Bad request" -// @Failure 401 {object} response.BaseResponse{data=errors.Error} "Unauthorized" -// @Router /api/v1/Partner [post] -// @Tags Partner APIs -func (h *Handler) Create(c *gin.Context) { - ctx := request.GetMyContext(c) - - var req request.CreatePartnerRequest - if err := c.ShouldBindJSON(&req); err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - validate := validator.New() - if err := validate.Struct(req); err != nil { - response.ErrorWrapper(c, err) - return - } - - res, err := h.service.Create(ctx, req.ToEntity()) - - if err != nil { - response.ErrorWrapper(c, err) - return - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: h.toPartnerResponse(res), - }) -} - -// Update handles the update of an existing Partner. -// @Summary Update an existing Partner -// @Description Update the details of an existing Partner based on the provided ID. -// @Accept json -// @Produce json -// @Param Authorization header string true "JWT token" -// @Param id path int64 true "Partner ID to update" -// @Param req body request.Partner true "Updated Partner details" -// @Success 200 {object} response.BaseResponse{data=response.Partner} "Partner updated successfully" -// @Failure 400 {object} response.BaseResponse{data=errors.Error} "Bad request" -// @Failure 401 {object} response.BaseResponse{data=errors.Error} "Unauthorized" -// @Router /api/v1/Partner/{id} [put] -// @Tags Partner APIs -func (h *Handler) Update(c *gin.Context) { - ctx := request.GetMyContext(c) - - id := c.Param("id") - - PartnerID, err := strconv.ParseInt(id, 10, 64) - if err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - var req request.Partner - if err := c.ShouldBindJSON(&req); err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - updatedPartner, err := h.service.Update(ctx, req.ToEntityUpdate(PartnerID)) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: h.toPartnerResponse(updatedPartner), - }) -} - -// GetAll retrieves a list of Partneres. -// @Summary Get a list of Partneres -// @Description Get a paginated list of Partneres based on query parameters. -// @Accept json -// @Produce json -// @Param Authorization header string true "JWT token" -// @Param Limit query int false "Number of items to retrieve (default 10)" -// @Param Offset query int false "Offset for pagination (default 0)" -// @Success 200 {object} response.BaseResponse{data=response.PartnerList} "List of Partneres" -// @Failure 400 {object} response.BaseResponse{data=errors.Error} "Bad request" -// @Failure 401 {object} response.BaseResponse{data=errors.Error} "Unauthorized" -// @Router /api/v1/Partner/list [get] -// @Tags Partner APIs -func (h *Handler) GetAll(c *gin.Context) { - var req request.PartnerParam - if err := c.ShouldBindQuery(&req); err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - ctx := request.GetMyContext(c) - - Partners, total, err := h.service.GetAll(c.Request.Context(), req.ToEntity(ctx)) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: h.toPartnerResponseList(Partners, int64(total), req), - }) -} - -// Delete handles the deletion of a Partner by ID. -// @Summary Delete a Partner by ID -// @Description Delete a Partner based on the provided ID. -// @Accept json -// @Produce json -// @Param Authorization header string true "JWT token" -// @Param id path int64 true "Partner ID to delete" -// @Success 200 {object} response.BaseResponse "Partner deleted successfully" -// @Failure 400 {object} response.BaseResponse{data=errors.Error} "Bad request" -// @Failure 401 {object} response.BaseResponse{data=errors.Error} "Unauthorized" -// @Router /api/v1/Partner/{id} [delete] -// @Tags Partner APIs -func (h *Handler) Delete(c *gin.Context) { - ctx := request.GetMyContext(c) - id := c.Param("id") - - // Parse the ID into a uint - PartnerID, err := strconv.ParseInt(id, 10, 64) - if err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - err = h.service.Delete(ctx, PartnerID) - if err != nil { - c.JSON(http.StatusInternalServerError, response.BaseResponse{ - Success: false, - Status: http.StatusInternalServerError, - Message: err.Error(), - Data: nil, - }) - return - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: nil, - }) -} - -// GetByID retrieves details of a specific Partner by ID. -// @Summary Get details of a Partner by ID -// @Description Get details of a Partner based on the provided ID. -// @Accept json -// @Produce json -// @Param Authorization header string true "JWT token" -// @Param id path int64 true "Partner ID to retrieve" -// @Success 200 {object} response.BaseResponse{data=response.Partner} "Partner details" -// @Failure 400 {object} response.BaseResponse{data=errors.Error} "Bad request" -// @Failure 401 {object} response.BaseResponse{data=errors.Error} "Unauthorized" -// @Router /api/v1/Partner/{id} [get] -// @Tags Partner APIs -func (h *Handler) GetByID(c *gin.Context) { - id := c.Param("id") - - // Parse the ID into a uint - PartnerID, err := strconv.ParseInt(id, 10, 64) - if err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - res, err := h.service.GetByID(c.Request.Context(), PartnerID) - if err != nil { - c.JSON(http.StatusInternalServerError, response.BaseResponse{ - Success: false, - Status: http.StatusInternalServerError, - Message: err.Error(), - Data: nil, - }) - return - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: h.toPartnerResponse(res), - }) -} - -func (h *Handler) toPartnerResponse(resp *entity.Partner) response.Partner { - return response.Partner{ - ID: &resp.ID, - Name: resp.Name, - Status: resp.Status, - CreatedAt: resp.CreatedAt.Format(time.RFC3339), - UpdatedAt: resp.CreatedAt.Format(time.RFC3339), - Balance: resp.Balance, - AdminName: resp.AdminName, - AdminPhoneNumber: resp.AdminPhoneNumber, - AdminEmail: resp.AdminEmail, - BankAccountName: resp.BankName, - BankAccountHolderName: resp.BankAccountHolderName, - BankAccountHolderNumber: resp.BankAccountNumber, - Logo: resp.Logo, - Address: resp.Address, - } -} - -func (h *Handler) toPartnerResponseList(resp []*entity.Partner, total int64, req request.PartnerParam) response.PartnerList { - var Partneres []response.Partner - for _, b := range resp { - Partneres = append(Partneres, h.toPartnerResponse(b)) - } - - return response.PartnerList{ - Partners: Partneres, - Total: total, - Limit: req.Limit, - Offset: req.Offset, - } -} - -func (h *Handler) UpdateMyStore(c *gin.Context) { - ctx := request.GetMyContext(c) - - PartnerID := ctx.GetPartnerID() - - var req request.Partner - if err := c.ShouldBindJSON(&req); err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - updatedPartner, err := h.service.Update(ctx, req.ToEntityUpdate(*PartnerID)) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: h.toPartnerResponse(updatedPartner), - }) -} diff --git a/internal/handlers/http/product/product.go b/internal/handlers/http/product/product.go deleted file mode 100644 index 1fa2a73..0000000 --- a/internal/handlers/http/product/product.go +++ /dev/null @@ -1,303 +0,0 @@ -package product - -import ( - "enaklo-pos-be/internal/common/errors" - "enaklo-pos-be/internal/entity" - "enaklo-pos-be/internal/handlers/request" - "enaklo-pos-be/internal/handlers/response" - "enaklo-pos-be/internal/services" - "fmt" - "github.com/gin-gonic/gin" - "github.com/go-playground/validator/v10" - "net/http" - "strconv" -) - -type Handler struct { - service services.Product -} - -func (h *Handler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) { - route := group.Group("/products") - - route.POST("/create", jwt, h.Create) - route.GET("/pos", jwt, h.GetPOSProduct) - route.GET("/list", jwt, h.GetAll) - route.PUT("/:id", jwt, h.Update) - route.GET("/:id", jwt, h.GetByID) - route.DELETE("/:id", jwt, h.Delete) -} - -func NewHandler(service services.Product) *Handler { - return &Handler{ - service: service, - } -} - -// Create handles the creation of a new product. -// @Summary Create a new product -// @Description Create a new product with the provided details. -// @Accept json -// @Produce json -// @Param Authorization header string true "JWT token" -// @Param req body request.Product true "Product details to create" -// @Success 200 {object} response.BaseResponse{data=response.Product} "Product created successfully" -// @Failure 400 {object} response.BaseResponse{data=errors.Error} "Bad request" -// @Failure 401 {object} response.BaseResponse{data=errors.Error} "Unauthorized" -// @Tags Product APIs -// @Router /api/v1/product/ [post] -func (h *Handler) Create(c *gin.Context) { - ctx := request.GetMyContext(c) - - var req request.Product - if err := c.ShouldBindJSON(&req); err != nil { - fmt.Println(err) - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - req.PartnerID = *ctx.GetPartnerID() - - res, err := h.service.Create(ctx, req.ToEntity()) - - if err != nil { - response.ErrorWrapper(c, err) - return - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: h.toProductResponse(res), - }) -} - -// Update handles the update of an existing product. -// @Summary Update an existing product -// @Description Update the details of an existing product based on the provided ID. -// @Accept json -// @Produce json -// @Param Authorization header string true "JWT token" -// @Param id path int64 true "Product ID to update" -// @Param req body request.Product true "Updated product details" -// @Success 200 {object} response.BaseResponse{data=response.Product} "Product updated successfully" -// @Failure 400 {object} response.BaseResponse{data=errors.Error} "Bad request" -// @Failure 401 {object} response.BaseResponse{data=errors.Error} "Unauthorized" -// @Tags Product APIs -// @Router /api/v1/product/{id} [put] -func (h *Handler) Update(c *gin.Context) { - ctx := request.GetMyContext(c) - - id := c.Param("id") - - productID, err := strconv.ParseInt(id, 10, 64) - if err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - var req request.Product - if err := c.ShouldBindJSON(&req); err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - validate := validator.New() - if err := validate.Struct(req); err != nil { - response.ErrorWrapper(c, err) - return - } - - updatedProduct, err := h.service.Update(ctx, productID, req.ToEntity()) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: h.toProductResponse(updatedProduct), - }) -} - -// GetAll retrieves a list of products. -// @Summary Get a list of products -// @Description Get a paginated list of products based on query parameters. -// @Accept json -// @Produce json -// @Param Authorization header string true "JWT token" -// @Param Limit query int false "Number of items to retrieve (default 10)" -// @Param Offset query int false "Offset for pagination (default 0)" -// @Success 200 {object} response.BaseResponse{data=response.ProductList} "List of products" -// @Failure 400 {object} response.BaseResponse{data=errors.Error} "Bad request" -// @Failure 401 {object} response.BaseResponse{data=errors.Error} "Unauthorized" -// @Router /api/v1/product/list [get] -// @Tags Product APIs -func (h *Handler) GetAll(c *gin.Context) { - var req request.ProductParam - if err := c.ShouldBindQuery(&req); err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - ctx := request.GetMyContext(c) - - products, total, err := h.service.GetAll(ctx, req.ToEntity(*ctx.GetPartnerID())) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: h.toProductResponseList(products, int64(total), req), - }) -} - -func (h *Handler) GetPOSProduct(c *gin.Context) { - ctx := request.GetMyContext(c) - - var req request.ProductParam - if err := c.ShouldBindQuery(&req); err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - if !ctx.IsCasheer() { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - products, err := h.service.GetProductPOS(c.Request.Context(), entity.ProductPOS{ - PartnerID: *ctx.GetPartnerID(), - SiteID: *ctx.GetSiteID(), - }) - - if err != nil { - response.ErrorWrapper(c, err) - return - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: h.toProductResponseList(products, int64(len(products)), req), - }) -} - -// Delete handles the deletion of a product by ID. -// @Summary Delete a product by ID -// @Description Delete a product based on the provided ID. -// @Accept json -// @Produce json -// @Param Authorization header string true "JWT token" -// @Param id path int64 true "Product ID to delete" -// @Success 200 {object} response.BaseResponse "Product deleted successfully" -// @Failure 400 {object} response.BaseResponse{data=errors.Error} "Bad request" -// @Failure 401 {object} response.BaseResponse{data=errors.Error} "Unauthorized" -// @Router /api/v1/product/{id} [delete] -// @Tags Product APIs -func (h *Handler) Delete(c *gin.Context) { - ctx := request.GetMyContext(c) - id := c.Param("id") - - // Parse the ID into a uint - productID, err := strconv.ParseInt(id, 10, 64) - if err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - err = h.service.Delete(ctx, productID) - if err != nil { - c.JSON(http.StatusInternalServerError, response.BaseResponse{ - Success: false, - Status: http.StatusInternalServerError, - Message: err.Error(), - Data: nil, - }) - return - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: nil, - }) -} - -// GetByID retrieves details of a specific product by ID. -// @Summary Get details of a product by ID -// @Description Get details of a product based on the provided ID. -// @Accept json -// @Produce json -// @Param Authorization header string true "JWT token" -// @Param id path int64 true "Product ID to retrieve" -// @Success 200 {object} response.BaseResponse{data=response.Product} "Product details" -// @Failure 400 {object} response.BaseResponse{data=errors.Error} "Bad request" -// @Failure 401 {object} response.BaseResponse{data=errors.Error} "Unauthorized" -// @Router /api/v1/product/{id} [get] -// @Tags Product APIs -func (h *Handler) GetByID(c *gin.Context) { - id := c.Param("id") - - // Parse the ID into a uint - productID, err := strconv.ParseInt(id, 10, 64) - if err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - res, err := h.service.GetByID(c.Request.Context(), productID) - if err != nil { - c.JSON(http.StatusInternalServerError, response.BaseResponse{ - Success: false, - Status: http.StatusInternalServerError, - Message: err.Error(), - Data: nil, - }) - return - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: h.toProductResponse(res), - }) -} - -func (h *Handler) toProductResponse(resp *entity.Product) response.Product { - category := response.Category{} - - if resp.Category != nil { - category.ID = resp.Category.ID - category.Name = resp.Category.Name - } - - return response.Product{ - ID: resp.ID, - Name: resp.Name, - Type: resp.Type, - Price: resp.Price, - Status: resp.Status, - Description: resp.Description, - Image: resp.Image, - Category: category, - } -} - -func (h *Handler) toProductResponseList(resp []*entity.Product, total int64, req request.ProductParam) response.ProductList { - var products []response.Product - for _, b := range resp { - products = append(products, h.toProductResponse(b)) - } - - return response.ProductList{ - Products: products, - Total: total, - Limit: req.Limit, - Offset: req.Offset, - } -} diff --git a/internal/handlers/http/sites/sites.go b/internal/handlers/http/sites/sites.go deleted file mode 100644 index 5f7571a..0000000 --- a/internal/handlers/http/sites/sites.go +++ /dev/null @@ -1,324 +0,0 @@ -package site - -import ( - "enaklo-pos-be/internal/common/errors" - "enaklo-pos-be/internal/entity" - "enaklo-pos-be/internal/handlers/request" - "enaklo-pos-be/internal/handlers/response" - "enaklo-pos-be/internal/services" - "net/http" - "strconv" - "time" - - "github.com/gin-gonic/gin" - "github.com/go-playground/validator/v10" -) - -type Handler struct { - service services.Site -} - -func (h *Handler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) { - route := group.Group("/site") - - route.POST("/", jwt, h.Create) - route.GET("/list", jwt, h.GetAll) - route.PUT("/:id", jwt, h.Update) - route.GET("/:id", jwt, h.GetByID) - route.DELETE("/:id", jwt, h.Delete) - route.GET("/count", jwt, h.Count) -} - -func NewHandler(service services.Site) *Handler { - return &Handler{ - service: service, - } -} - -// Create handles the creation of a new Site. -// @Summary Create a new Site -// @Description Create a new Site based on the provided data. -// @Accept json -// @Produce json -// @Param Authorization header string true "JWT token" -// @Param req body request.Site true "New Site details" -// @Success 200 {object} response.BaseResponse{data=response.Site} "Site created successfully" -// @Failure 400 {object} response.BaseResponse{data=errors.Error} "Bad request" -// @Failure 401 {object} response.BaseResponse{data=errors.Error} "Unauthorized" -// @Router /api/v1/site [post] -// @Tags Site APIs -func (h *Handler) Create(c *gin.Context) { - ctx := request.GetMyContext(c) - - var req request.Site - if err := c.ShouldBindJSON(&req); err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - if !ctx.IsAdmin() { - req.PartnerID = ctx.GetPartnerID() - } - - validate := validator.New() - if err := validate.Struct(req); err != nil { - response.ErrorWrapper(c, err) - return - } - - res, err := h.service.Create(ctx, req.ToEntity(ctx.RequestedBy())) - - if err != nil { - response.ErrorWrapper(c, err) - return - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: h.toSiteResponse(res), - }) -} - -// Update handles the update of an existing Site. -// @Summary Update an existing Site -// @Description Update the details of an existing Site based on the provided ID. -// @Accept json -// @Produce json -// @Param Authorization header string true "JWT token" -// @Param id path int64 true "Site ID to update" -// @Param req body request.Site true "Updated Site details" -// @Success 200 {object} response.BaseResponse{data=response.Site} "Site updated successfully" -// @Failure 400 {object} response.BaseResponse{data=errors.Error} "Bad request" -// @Failure 401 {object} response.BaseResponse{data=errors.Error} "Unauthorized" -// @Router /api/v1/site/{id} [put] -// @Tags Site APIs -func (h *Handler) Update(c *gin.Context) { - ctx := request.GetMyContext(c) - - id := c.Param("id") - - SiteID, err := strconv.ParseInt(id, 10, 64) - if err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - var req request.Site - if err := c.ShouldBindJSON(&req); err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - validate := validator.New() - if err := validate.Struct(req); err != nil { - response.ErrorWrapper(c, err) - return - } - - updatedSite, err := h.service.Update(ctx, SiteID, req.ToEntity(ctx.RequestedBy())) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: h.toSiteResponse(updatedSite), - }) -} - -// GetAll retrieves a list of Sites. -// @Summary Get a list of Sites -// @Description Get a paginated list of Sites based on query parameters. -// @Accept json -// @Produce json -// @Param Authorization header string true "JWT token" -// @Param Limit query int false "Number of items to retrieve (default 10)" -// @Param Offset query int false "Offset for pagination (default 0)" -// @Success 200 {object} response.BaseResponse{data=response.SiteList} "List of Sites" -// @Failure 400 {object} response.BaseResponse{data=errors.Error} "Bad request" -// @Failure 401 {object} response.BaseResponse{data=errors.Error} "Unauthorized" -// @Router /api/v1/site/list [get] -// @Tags Site APIs -func (h *Handler) GetAll(c *gin.Context) { - var req request.SiteParam - if err := c.ShouldBindQuery(&req); err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - ctx := request.GetMyContext(c) - Sites, total, err := h.service.GetAll(c.Request.Context(), req.ToEntity(ctx, ctx.GetPartnerID(), ctx.GetSiteID())) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: h.toSiteResponseList(Sites, int64(total), req), - }) -} - -// Delete handles the deletion of a Site by ID. -// @Summary Delete a Site by ID -// @Description Delete a Site based on the provided ID. -// @Accept json -// @Produce json -// @Param Authorization header string true "JWT token" -// @Param id path int64 true "Site ID to delete" -// @Success 200 {object} response.BaseResponse "Site deleted successfully" -// @Failure 400 {object} response.BaseResponse{data=errors.Error} "Bad request" -// @Failure 401 {object} response.BaseResponse{data=errors.Error} "Unauthorized" -// @Router /api/v1/site/{id} [delete] -// @Tags Site APIs -func (h *Handler) Delete(c *gin.Context) { - ctx := request.GetMyContext(c) - id := c.Param("id") - - // Parse the ID into a uint - SiteID, err := strconv.ParseInt(id, 10, 64) - if err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - err = h.service.Delete(ctx, SiteID) - if err != nil { - c.JSON(http.StatusInternalServerError, response.BaseResponse{ - Success: false, - Status: http.StatusInternalServerError, - Message: err.Error(), - Data: nil, - }) - return - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: nil, - }) -} - -// GetByID retrieves details of a specific Site by ID. -// @Summary Get details of a Site by ID -// @Description Get details of a Site based on the provided ID. -// @Accept json -// @Produce json -// @Param Authorization header string true "JWT token" -// @Param id path int64 true "Site ID to retrieve" -// @Success 200 {object} response.BaseResponse{data=response.Site} "Site details" -// @Failure 400 {object} response.BaseResponse{data=errors.Error} "Bad request" -// @Failure 401 {object} response.BaseResponse{data=errors.Error} "Unauthorized" -// @Router /api/v1/site/{id} [get] -// @Tags Site APIs -func (h *Handler) GetByID(c *gin.Context) { - id := c.Param("id") - - // Parse the ID into a uint - SiteID, err := strconv.ParseInt(id, 10, 64) - if err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - res, err := h.service.GetByID(c.Request.Context(), SiteID) - if err != nil { - c.JSON(http.StatusInternalServerError, response.BaseResponse{ - Success: false, - Status: http.StatusInternalServerError, - Message: err.Error(), - Data: nil, - }) - return - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: h.toSiteResponse(res), - }) -} - -func (h *Handler) Count(c *gin.Context) { - var req request.SiteParam - if err := c.ShouldBindQuery(&req); err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - ctx := request.GetMyContext(c) - res, err := h.service.Count(ctx, req.ToEntity(ctx, ctx.GetPartnerID(), ctx.GetSiteID())) - - if err != nil { - response.ErrorWrapper(c, err) - return - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: response.SiteCount{ - Count: res.Count, - }, - }) -} - -func (h *Handler) toSiteResponse(resp *entity.Site) response.Site { - return response.Site{ - ID: &resp.ID, - Name: resp.Name, - PartnerID: resp.PartnerID, - Image: resp.Image, - Address: resp.Address, - LocationLink: resp.LocationLink, - Description: resp.Description, - Highlight: resp.Highlight, - ContactPerson: resp.ContactPerson, - TnC: resp.TnC, - AdditionalInfo: resp.AdditionalInfo, - Status: resp.Status, - IsSeasonTicket: resp.IsSeasonTicket, - IsDiscountActive: resp.IsDiscountActive, - CreatedAt: resp.CreatedAt.Format(time.RFC3339), - UpdatedAt: resp.UpdatedAt.Format(time.RFC3339), - Products: h.toProductResponseList(resp.Products), - Lat: *resp.Latitude, - Long: *resp.Longitude, - Region: resp.Region, - Regency: resp.Regency, - } -} - -func (h *Handler) toSiteResponseList(resp []*entity.Site, total int64, req request.SiteParam) response.SiteList { - sites := []response.Site{} - for _, b := range resp { - sites = append(sites, h.toSiteResponse(b)) - } - - return response.SiteList{ - Sites: sites, - Total: total, - Limit: req.Limit, - Offset: req.Offset, - } -} - -func (h *Handler) toProductResponseList(products []entity.Product) []response.Product { - var res []response.Product - for _, product := range products { - res = append(res, response.Product{ - ID: product.ID, - Name: product.Name, - Type: product.Type, - Price: product.Price, - Status: product.Status, - Description: product.Description, - }) - } - return res -} diff --git a/internal/handlers/http/transaction/transaction.go b/internal/handlers/http/transaction/transaction.go deleted file mode 100644 index ccad3cd..0000000 --- a/internal/handlers/http/transaction/transaction.go +++ /dev/null @@ -1,119 +0,0 @@ -package transaction - -import ( - "enaklo-pos-be/internal/common/errors" - "enaklo-pos-be/internal/entity" - "enaklo-pos-be/internal/handlers/request" - "enaklo-pos-be/internal/handlers/response" - "enaklo-pos-be/internal/services" - "github.com/gin-gonic/gin" - "net/http" -) - -type TransactionHandler struct { - service services.Transaction -} - -func (h *TransactionHandler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) { - route := group.Group("/transaction") - - route.GET("/search", jwt, h.Search) - route.POST("/approval", jwt, h.Approval) -} - -func New(service services.Transaction) *TransactionHandler { - return &TransactionHandler{ - service: service, - } -} - -// Search retrieves a list of studios based on search criteria. -// @Summary Search for studios -// @Description Search for studios based on query parameters. -// @Accept json -// @Produce json -// @Param Authorization header string true "JWT token" -// @Param Name query string false "Studio name for search" -// @Param Status query string false "Studio status for search" -// @Param Limit query int false "Number of items to retrieve (default 10)" -// @Param Offset query int false "Offset for pagination (default 0)" -// @Success 200 {object} response.BaseResponse{data=response.StudioList} "List of studios" -// @Failure 400 {object} response.BaseResponse{data=errors.Error} "Bad request" -// @Failure 401 {object} response.BaseResponse{data=errors.Error} "Unauthorized" -// @Router /api/v1/studio/search [get] -// @Tags Studio APIs -func (h *TransactionHandler) Search(c *gin.Context) { - var req request.TransactionSearch - if err := c.ShouldBindQuery(&req); err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - ctx := request.GetMyContext(c) - - transactions, total, err := h.service.GetTransactionList(ctx, req.ToEntity(ctx)) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: h.ToTransactionListResponse(transactions, total, req.Limit, req.Offset), - }) -} - -func (h *TransactionHandler) Approval(c *gin.Context) { - var req request.ApprovalRequest - if err := c.ShouldBindJSON(&req); err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - ctx := request.GetMyContext(c) - - if !ctx.IsAdmin() { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - if err := req.Validate(); err != nil { - response.ErrorWrapper(c, errors.NewError(errors.ErrorBadRequest.ErrorType(), err.Error())) - return - } - - err := h.service.Approval(ctx, req.ToEntity()) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - }) -} - -func (h *TransactionHandler) ToTransactionListResponse(transactions []*entity.TransactionList, totalCount, limit, offset int) response.TransactionListResponse { - responseItems := make([]response.TransactionListItem, len(transactions)) - for i, transaction := range transactions { - responseItems[i] = response.TransactionListItem{ - ID: transaction.ID, - TransactionType: transaction.TransactionType, - Status: transaction.Status, - CreatedAt: transaction.CreatedAt.Format("2006-01-02 15:04:05"), - SiteName: transaction.SiteName, - Amount: transaction.Amount, - PartnerName: transaction.PartnerName, - Total: transaction.Total, - Fee: transaction.Fee, - } - } - - return response.TransactionListResponse{ - Transactions: responseItems, - TotalCount: totalCount, - Limit: limit, - Offset: offset, - } -} diff --git a/internal/handlers/http/user/user.go b/internal/handlers/http/user/user.go deleted file mode 100644 index f33345d..0000000 --- a/internal/handlers/http/user/user.go +++ /dev/null @@ -1,533 +0,0 @@ -package user - -import ( - "enaklo-pos-be/internal/constants/role" - "fmt" - "net/http" - "strconv" - "strings" - "time" - - "github.com/gin-gonic/gin" - "github.com/go-playground/validator/v10" - "github.com/xuri/excelize/v2" - - "enaklo-pos-be/internal/common/errors" - "enaklo-pos-be/internal/entity" - "enaklo-pos-be/internal/handlers/request" - "enaklo-pos-be/internal/handlers/response" - "enaklo-pos-be/internal/services" -) - -type Handler struct { - service services.User -} - -func (h *Handler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) { - route := group.Group("/user") - - route.POST("/", jwt, h.Create) - route.POST("/customer", jwt, h.CreateCustomer) - route.POST("/customer-bulk-excel", jwt, h.CreateCustomersFromExcel) - route.GET("/list", jwt, h.GetAll) - route.GET("/customer/list", jwt, h.GetAllCustomer) - route.GET("/:id", jwt, h.GetByID) - route.PUT("/:id", jwt, h.Update) - route.PUT("/customer/:id", jwt, h.UpdateCustomer) - route.DELETE("/customer/:id", jwt, h.DeleteCustomer) - route.DELETE("/:id", jwt, h.Delete) -} - -func NewHandler(service services.User) *Handler { - return &Handler{ - service: service, - } -} - -// Create handles the creation of a new user. -// @Summary Create a new user -// @Description Create a new user based on the provided data. -// @Accept json -// @Produce json -// @Param Authorization header string true "JWT token" -// @Param req body request.User true "New user details" -// @Success 200 {object} response.BaseResponse{data=response.User} "User created successfully" -// @Failure 400 {object} response.BaseResponse{data=errors.Error} "Bad request" -// @Failure 401 {object} response.BaseResponse{data=errors.Error} "Unauthorized" -// @Router /api/v1/user [post] -// @Tags User APIs -func (h *Handler) Create(c *gin.Context) { - ctx := request.GetMyContext(c) - - var req request.User - if err := c.ShouldBindJSON(&req); err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - if !ctx.IsSuperAdmin() { - req.PartnerID = ctx.GetPartnerID() - if err := req.Validate(); err != nil { - response.ErrorWrapper(c, errors.ErrorInvalidRequest) - return - } - } - - if req.RoleID == role.Casheer && req.SiteID == nil { - response.ErrorWrapper(c, errors.NewServiceException("site id is required for cashier")) - return - } - - res, err := h.service.Create(ctx, req.ToEntity(ctx, "")) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - resp := response.User{ - ID: res.ID, - Name: res.Name, - Email: res.Email, - RoleID: int64(res.RoleID), - PartnerID: res.PartnerID, - Status: string(res.Status), - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: resp, - }) -} - -func (h *Handler) CreateCustomer(c *gin.Context) { - ctx := request.GetMyContext(c) - - var req request.User - if err := c.ShouldBindJSON(&req); err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - res, err := h.service.Create(ctx, req.ToEntity(ctx, "CUSTOMER")) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - resp := response.User{ - ID: res.ID, - Name: res.Name, - Email: res.Email, - RoleID: int64(res.RoleID), - PartnerID: res.PartnerID, - Status: string(res.Status), - PhoneNumber: res.PhoneNumber, - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: resp, - }) -} - -func (h *Handler) CreateCustomersFromExcel(c *gin.Context) { - ctx := request.GetMyContext(c) - - // var req request.User - var responseData []response.User - - file, err := c.FormFile("file") - if err != nil { - fmt.Println("err", err.Error()) - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - f, err := file.Open() - if err != nil { - fmt.Println("err", err.Error()) - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - defer f.Close() - - excel, err := excelize.OpenReader(f) - if err != nil { - fmt.Println("err", err.Error()) - response.ErrorWrapper(c, errors.ErrorBadRequest) - // c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "Failed to read Excel file"}) - return - } - - // sheetName := excel.GetSheetName(1) // Assuming the data is in the first sheet - rows, err := excel.GetRows("Sheet1") - if err != nil { - fmt.Println("err", err.Error()) - response.ErrorWrapper(c, errors.ErrorBadRequest) - // c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "Failed to read rows from Excel"}) - return - } - - var phoneNumbers []string - - for _, row := range rows { - if len(row) > 0 { - phoneNumber := strings.TrimSpace(row[0]) - if phoneNumber != "" { - phoneNumbers = append(phoneNumbers, phoneNumber) - } - } - } - - // c.JSON(http.StatusOK, response.BaseResponse{ - // Success: true, - // Status: http.StatusOK, - // Data: phoneNumbers, - // }) - // return - - if len(phoneNumbers) == 0 { - response.ErrorWrapper(c, errors.ErrorBadRequest) - // c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "No phone numbers found in the Excel file"}) - return - } - - // var createdCustomers []CustomerResponse - for _, phone := range phoneNumbers { - req := request.User{ - PhoneNumber: phone, - Email: "", - } - - res, err := h.service.Create(ctx, req.ToEntity(ctx, "CUSTOMER")) - // res, err := h.service.Create(c, customer) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{ - "success": false, - "message": fmt.Sprintf("Failed to create customer for phone number %s: %v", phone, err), - }) - return - } - - responseData = append(responseData, response.User{PhoneNumber: res.PhoneNumber}) - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: responseData, - }) -} - -// Update handles the update of an existing user. -// @Summary Update an existing user -// @Description Update the details of an existing user based on the provided ID. -// @Accept json -// @Produce json -// @Param Authorization header string true "JWT token" -// @Param id path int64 true "User ID to update" -// @Param req body request.User true "Updated user details" -// @Success 200 {object} response.BaseResponse{data=response.User} "User updated successfully" -// @Failure 400 {object} response.BaseResponse{data=errors.Error} "Bad request" -// @Failure 401 {object} response.BaseResponse{data=errors.Error} "Unauthorized" -// @Router /api/v1/user/{id} [put] -// @Tags User APIs -func (h *Handler) Update(c *gin.Context) { - ctx := request.GetMyContext(c) - - id := c.Param("id") - - userID, err := strconv.ParseInt(id, 10, 64) - if err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - var req request.User - if err := c.ShouldBindJSON(&req); err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - validate := validator.New() - if err := validate.Struct(req); err != nil { - response.ErrorWrapper(c, err) - return - } - - updatedUser, err := h.service.Update(ctx, userID, req.ToEntity(ctx, "")) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: h.toUserResponse(updatedUser), - }) -} -func (h *Handler) UpdateCustomer(c *gin.Context) { - ctx := request.GetMyContext(c) - - id := c.Param("id") - - userID, err := strconv.ParseInt(id, 10, 64) - if err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - var req request.User - if err := c.ShouldBindJSON(&req); err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - updatedUser, err := h.service.Update(ctx, userID, req.ToEntity(ctx, "CUSTOMER")) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: h.toUserResponse(updatedUser), - }) -} - -// GetAll retrieves a list of users. -// @Summary Get a list of users -// @Description Get a paginated list of users based on query parameters. -// @Accept json -// @Produce json -// @Param Authorization header string true "JWT token" -// @Param Limit query int false "Number of items to retrieve (default 10)" -// @Param Offset query int false "Offset for pagination (default 0)" -// @Success 200 {object} response.BaseResponse{data=response.UserList} "List of users" -// @Failure 400 {object} response.BaseResponse{data=errors.Error} "Bad request" -// @Failure 401 {object} response.BaseResponse{data=errors.Error} "Unauthorized" -// @Router /api/v1/user/list [get] -// @Tags User APIs -func (h *Handler) GetAll(c *gin.Context) { - ctx := request.GetMyContext(c) - var req request.UserParam - if err := c.ShouldBindQuery(&req); err != nil { - response.ErrorWrapper(c, err) - return - } - - users, total, err := h.service.GetAll(ctx, req.ToEntity(ctx)) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: h.toUserResponseList(users, int64(total), req), - }) -} - -func (h *Handler) GetAllCustomer(c *gin.Context) { - ctx := request.GetMyContext(c) - var req request.CustomerParam - if err := c.ShouldBindQuery(&req); err != nil { - response.ErrorWrapper(c, err) - return - } - - users, total, err := h.service.GetAllCustomer(ctx, req.ToEntity(ctx)) - if err != nil { - response.ErrorWrapper(c, err) - return - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: h.toCustomerResponseList(users, int64(total), req), - }) -} - -// GetByID retrieves details of a specific user by ID. -// @Summary Get details of a user by ID -// @Description Get details of a user based on the provided ID. -// @Accept json -// @Produce json -// @Param Authorization header string true "JWT token" -// @Param id path int64 true "User ID to retrieve" -// @Success 200 {object} response.BaseResponse{data=response.User} "User details" -// @Failure 400 {object} response.BaseResponse{data=errors.Error} "Bad request" -// @Failure 401 {object} response.BaseResponse{data=errors.Error} "Unauthorized" -// @Router /api/v1/user/{id} [get] -// @Tags User APIs -func (h *Handler) GetByID(c *gin.Context) { - ctx := request.GetMyContext(c) - id := c.Param("id") - - // Parse the ID into a uint - userID, err := strconv.ParseInt(id, 10, 64) - if err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - res, err := h.service.GetByID(ctx, userID) - if err != nil { - c.JSON(http.StatusInternalServerError, response.BaseResponse{ - Success: false, - Status: http.StatusInternalServerError, - Message: err.Error(), - Data: nil, - }) - return - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: h.toUserResponse(res), - }) -} - -// Delete handles the deletion of a user by ID. -// @Summary Delete a user by ID -// @Description Delete a user based on the provided ID. -// @Accept json -// @Produce json -// @Param Authorization header string true "JWT token" -// @Param id path int64 true "User ID to delete" -// @Success 200 {object} response.BaseResponse "User deleted successfully" -// @Failure 400 {object} response.BaseResponse{data=errors.Error} "Bad request" -// @Failure 401 {object} response.BaseResponse{data=errors.Error} "Unauthorized" -// @Router /api/v1/user/{id} [delete] -// @Tags User APIs -func (h *Handler) Delete(c *gin.Context) { - ctx := request.GetMyContext(c) - id := c.Param("id") - - // Parse the ID into a uint - userID, err := strconv.ParseInt(id, 10, 64) - if err != nil { - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - err = h.service.Delete(ctx, userID) - if err != nil { - c.JSON(http.StatusInternalServerError, response.BaseResponse{ - Success: false, - Status: http.StatusInternalServerError, - Message: err.Error(), - Data: nil, - }) - return - } - - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Data: nil, - }) -} - -func (h *Handler) DeleteCustomer(c *gin.Context) { - ctx := request.GetMyContext(c) - id := c.Param("id") - - // Parse the ID into an int64 (or whatever type is used for customer ID) - customerID, err := strconv.ParseInt(id, 10, 64) - if err != nil { - // Handle invalid ID format error - response.ErrorWrapper(c, errors.ErrorBadRequest) - return - } - - // Call the service to delete the customer - err = h.service.Delete(ctx, customerID) - if err != nil { - // If there was an error deleting, return a 500 Internal Server Error - c.JSON(http.StatusInternalServerError, response.BaseResponse{ - Success: false, - Status: http.StatusInternalServerError, - Message: err.Error(), - Data: nil, - }) - return - } - - // Return a success response after deletion - c.JSON(http.StatusOK, response.BaseResponse{ - Success: true, - Status: http.StatusOK, - Message: "Customer deleted successfully", // Added success message - Data: nil, - }) -} - -func (h *Handler) toUserResponse(resp *entity.User) response.User { - return response.User{ - ID: resp.ID, - Name: resp.Name, - Email: resp.Email, - PhoneNumber: resp.PhoneNumber, - NIK: resp.NIK, - Status: string(resp.Status), - RoleID: int64(resp.RoleID), - RoleName: resp.RoleName, - PartnerID: resp.PartnerID, - PartnerName: resp.PartnerName, - CreatedAt: resp.CreatedAt.Format(time.RFC3339), - UpdatedAt: resp.CreatedAt.Format(time.RFC3339), - SiteID: resp.SiteID, - } -} - -func (h *Handler) toUserResponseList(resp []*entity.User, total int64, req request.UserParam) response.UserList { - var users []response.User - for _, b := range resp { - users = append(users, h.toUserResponse(b)) - } - - return response.UserList{ - Users: users, - Total: total, - Limit: req.Limit, - Offset: req.Offset, - } -} -func (h *Handler) toCustomerResponse(resp *entity.Customer) response.Customer { - return response.Customer{ - ID: resp.ID, - Name: resp.Name, - Email: resp.Email, - PhoneNumber: resp.PhoneNumber, - Status: string(resp.Status), - RoleID: int64(resp.RoleID), - RoleName: resp.RoleName, - PartnerID: resp.PartnerID, - PartnerName: resp.PartnerName, - CreatedAt: resp.CreatedAt.Format(time.RFC3339), - UpdatedAt: resp.CreatedAt.Format(time.RFC3339), - } -} - -func (h *Handler) toCustomerResponseList(resp []*entity.Customer, total int64, req request.CustomerParam) response.CustomerList { - var users []response.Customer - for _, b := range resp { - users = append(users, h.toCustomerResponse(b)) - } - - return response.CustomerList{ - Users: users, - Total: total, - Limit: req.Limit, - Offset: req.Offset, - } -} diff --git a/internal/handlers/request/auth.go b/internal/handlers/request/auth.go deleted file mode 100644 index 8288875..0000000 --- a/internal/handlers/request/auth.go +++ /dev/null @@ -1,78 +0,0 @@ -package request - -import ( - "errors" - "fmt" - "github.com/go-playground/validator/v10" -) - -type LoginRequest struct { - Email string `json:"email"` - Password string `json:"password"` -} - -type ResetPasswordRequest struct { - Email string `json:"email" validate:"required,email"` -} - -type ResetPasswordChangeRequest struct { - OldPassword string `json:"old_password" validate:"required"` - NewPassword string `json:"new_password" validate:"required,strongpwd"` -} - -func (e *ResetPasswordChangeRequest) Validate() error { - validate := validator.New() - - validate.RegisterValidation("strongpwd", validateStrongPassword) - - if err := validate.Struct(e); err != nil { - // Handle the validation errors - for _, err := range err.(validator.ValidationErrors) { - switch err.Field() { - case "NewPassword": - return fmt.Errorf("%w", validatePasswordError(err.Tag())) - default: - return fmt.Errorf("validation failed: %w", err) - } - } - } - - return nil -} - -func validateStrongPassword(fl validator.FieldLevel) bool { - password := fl.Field().String() - - var ( - hasMinLen = len(password) >= 8 - ) - - return hasMinLen -} - -// Error messages for password validation -var ( - ErrPasswordTooShort = errors.New("password must be at least 8 characters long") - ErrPasswordNoUpper = errors.New("password must contain at least one uppercase letter") - ErrPasswordNoLower = errors.New("password must contain at least one lowercase letter") - ErrPasswordNoNumber = errors.New("password must contain at least one digit") - ErrPasswordNoSpecial = errors.New("password must contain at least one special character (!@#$%^&*)") - ErrPasswordValidation = errors.New("password does not meet the strength requirements") -) - -func validatePasswordError(tag string) error { - switch tag { - case "min": - return ErrPasswordTooShort - case "uppercase": - return ErrPasswordNoUpper - case "lowercase": - return ErrPasswordNoLower - case "number": - return ErrPasswordNoNumber - case "special": - return ErrPasswordNoSpecial - default: - return ErrPasswordValidation - } -} diff --git a/internal/handlers/request/balance.go b/internal/handlers/request/balance.go deleted file mode 100644 index 3866c61..0000000 --- a/internal/handlers/request/balance.go +++ /dev/null @@ -1,23 +0,0 @@ -package request - -import "enaklo-pos-be/internal/entity" - -type BalanceReq struct { - Amount int64 `json:"amount"` - Token string `json:"token"` -} - -func (b *BalanceReq) ToEntity(partnerID int64) *entity.BalanceWithdrawInquiry { - return &entity.BalanceWithdrawInquiry{ - PartnerID: partnerID, - Amount: b.Amount, - } -} - -func (b *BalanceReq) ToEntityReq(partnerID int64) *entity.WalletWithdrawRequest { - return &entity.WalletWithdrawRequest{ - PartnerID: partnerID, - Amount: b.Amount, - Token: b.Token, - } -} diff --git a/internal/handlers/request/cashier.go b/internal/handlers/request/cashier.go deleted file mode 100644 index 9a9e04d..0000000 --- a/internal/handlers/request/cashier.go +++ /dev/null @@ -1,20 +0,0 @@ -package request - -import "enaklo-pos-be/internal/entity" - -type OpenCashierSessionRequest struct { - PartnerID int64 `json:"partner_id" validate:"required"` - OpeningAmount float64 `json:"opening_amount" validate:"required,gt=0"` -} - -type CloseCashierSessionRequest struct { - ClosingAmount float64 `json:"closing_amount" validate:"required"` -} - -func (o *OpenCashierSessionRequest) ToEntity(cashierID int64) *entity.CashierSession { - return &entity.CashierSession{ - PartnerID: o.PartnerID, - CashierID: cashierID, - OpeningAmount: o.OpeningAmount, - } -} diff --git a/internal/handlers/request/category.go b/internal/handlers/request/category.go deleted file mode 100644 index 0d83701..0000000 --- a/internal/handlers/request/category.go +++ /dev/null @@ -1,14 +0,0 @@ -package request - -import "enaklo-pos-be/internal/entity" - -type CategoryRequest struct { - Name string `json:"name" binding:"required"` -} - -func (r *CategoryRequest) ToEntity(partnerID int64) *entity.Category { - return &entity.Category{ - PartnerID: partnerID, - Name: r.Name, - } -} diff --git a/internal/handlers/request/context.go b/internal/handlers/request/context.go deleted file mode 100644 index e44b5fa..0000000 --- a/internal/handlers/request/context.go +++ /dev/null @@ -1,20 +0,0 @@ -package request - -import ( - "enaklo-pos-be/internal/common/mycontext" - "github.com/gin-gonic/gin" -) - -func GetMyContext(c *gin.Context) mycontext.Context { - rawCtx, exists := c.Get("myCtx") - if !exists { - return mycontext.NewContext(c) - } - - myCtx, ok := rawCtx.(mycontext.Context) - if !ok { - return mycontext.NewContext(c) - } - - return myCtx -} diff --git a/internal/handlers/request/customer.go b/internal/handlers/request/customer.go deleted file mode 100644 index edcde02..0000000 --- a/internal/handlers/request/customer.go +++ /dev/null @@ -1,49 +0,0 @@ -package request - -import ( - "enaklo-pos-be/internal/entity" - "github.com/go-playground/validator/v10" - "strings" - "time" -) - -type CustomerRegister struct { - Name string `json:"name" validate:"required" binding:"required"` - Email string `json:"email" validate:"required" binding:"required"` - PhoneNumber string `json:"phone_number" validate:"required" binding:"required"` - BirthDate string `json:"birth_date" validate:"required" binding:"required"` - Password string `json:"password" validate:"required" binding:"required"` -} - -func (c *CustomerRegister) Validate() error { - validate := validator.New() - if err := validate.Struct(c); err != nil { - return err - } - - return nil -} - -func (c *CustomerRegister) GetBirthdate() (time.Time, error) { - parsedDate, err := time.Parse("02-01-2006", c.BirthDate) - if err != nil { - return time.Time{}, err - } - return parsedDate, nil -} - -func (c *CustomerRegister) ToEntity() *entity.Customer { - birthdate, _ := c.GetBirthdate() - return &entity.Customer{ - Name: c.Name, - Email: strings.ToLower(c.Email), - PhoneNumber: c.PhoneNumber, - Password: c.Password, - BirthDate: birthdate, - } -} - -type VerifyEmailRequest struct { - VerificationID string `json:"verification_id" binding:"required"` - OTPCode string `json:"otp_code" binding:"required"` -} diff --git a/internal/handlers/request/discovery.go b/internal/handlers/request/discovery.go deleted file mode 100644 index a29bf33..0000000 --- a/internal/handlers/request/discovery.go +++ /dev/null @@ -1,37 +0,0 @@ -package request - -import ( - "enaklo-pos-be/internal/entity" -) - -type DiscoveryHomeParam struct { - Lat float64 `form:"lat" json:"lat" example:"10"` - Long float64 `form:"long" json:"long" example:"0"` - Name string `form:"name" json:"name" example:"0"` - Region string `form:"region" json:"region" example:"0"` - Radius int `form:"radius" json:"radius" example:"0"` - Limit int `form:"limit" json:"limit" example:"0"` - Offset int `form:"offset" json:"offset" example:"0"` - Discover string `form:"discover" json:"discover" example:"0"` -} - -type DiscoverySearchByID struct { - ID int64 `form:"id" json:"id" example:"0"` -} - -func (d *DiscoveryHomeParam) ToEntity() *entity.DiscoverySearch { - if d.Limit == 0 { - d.Limit = 10 - } - - return &entity.DiscoverySearch{ - Lat: d.Lat, - Long: d.Long, - Name: d.Name, - Region: d.Region, - Radius: d.Radius, - Limit: d.Limit, - Offset: d.Offset, - Discover: d.Discover, - } -} diff --git a/internal/handlers/request/event.go b/internal/handlers/request/event.go deleted file mode 100644 index c326505..0000000 --- a/internal/handlers/request/event.go +++ /dev/null @@ -1,86 +0,0 @@ -package request - -import ( - "fmt" - "time" - - "github.com/go-playground/validator/v10" - - "enaklo-pos-be/internal/entity" -) - -type EventParam struct { - Name string `form:"name" json:"name" example:"Ketua Umum"` - Limit int `form:"limit" json:"limit" example:"10"` - Offset int `form:"offset" json:"offset" example:"0"` -} - -func (p *EventParam) ToEntity() entity.EventSearch { - return entity.EventSearch{ - Name: p.Name, - Limit: p.Limit, - Offset: p.Offset, - } -} - -type Event struct { - Name string `json:"name" validate:"required"` - Description string `json:"description"` - StartDate string `json:"start_date" validate:"required"` - StartTime string `json:"start_time"` - EndDate string `json:"end_date" validate:"required"` - EndTime string `json:"end_time"` - Location string `json:"location" validate:"required"` - Level string `json:"level" validate:"required"` - Included []string `json:"included"` - Price float64 `json:"price"` - Paid bool `json:"paid"` - Status entity.Status `json:"status"` - LocationID int64 `json:"location_id"` - startDateTime time.Time - endDateTime time.Time -} - -func (e *Event) ToEntity() *entity.Event { - return &entity.Event{ - Name: e.Name, - Description: e.Description, - StartDate: e.startDateTime, - EndDate: e.endDateTime, - Location: e.Location, - Level: e.Level, - Included: e.Included, - Price: e.Price, - Paid: e.Paid, - LocationID: &e.LocationID, - Status: e.Status, - } -} - -func (e *Event) Validate() error { - validate := validator.New() - if err := validate.Struct(e); err != nil { - return err - } - - startDateTimeStr := e.StartDate + "T" + e.StartTime + "Z" - endDateTimeStr := e.EndDate + "T" + e.EndTime + "Z" - - startDateTime, err := time.Parse(time.RFC3339, startDateTimeStr) - if err != nil { - fmt.Println("Error parsing start date-time:", err) - return err - } - - e.startDateTime = startDateTime - - endDateTime, err := time.Parse(time.RFC3339, endDateTimeStr) - if err != nil { - fmt.Println("Error parsing end date-time:", err) - return err - } - - e.endDateTime = endDateTime - - return nil -} diff --git a/internal/handlers/request/license.go b/internal/handlers/request/license.go deleted file mode 100644 index efb778f..0000000 --- a/internal/handlers/request/license.go +++ /dev/null @@ -1,52 +0,0 @@ -package request - -import ( - "enaklo-pos-be/internal/entity" - "time" - - "github.com/google/uuid" -) - -type License struct { - Name string `json:"name" validate:"required"` - StartDate string `json:"start_date" validate:"required"` - EndDate string `json:"end_date" validate:"required"` - RenewalDate string `json:"renewal_date"` - SerialNumber string `json:"serial_number" validate:"required"` - PartnerID int64 `json:"partner_id" validate:"required"` -} - -type LicenseParam struct { - Limit int `form:"limit,default=10"` - Offset int `form:"offset,default=0"` - Status string `form:"status,default="` -} - -func (r *License) ToEntity() (*entity.License, error) { - startDate, err := time.Parse("2006-01-02", r.StartDate) - if err != nil { - return nil, err - } - endDate, err := time.Parse("2006-01-02", r.EndDate) - if err != nil { - return nil, err - } - var renewalDate *time.Time - if r.RenewalDate != "" { - parsedRenewalDate, err := time.Parse("2006-01-02", r.RenewalDate) - if err != nil { - return nil, err - } - renewalDate = &parsedRenewalDate - } - - return &entity.License{ - ID: uuid.New(), - Name: r.Name, - StartDate: startDate, - EndDate: endDate, - RenewalDate: renewalDate, - SerialNumber: r.SerialNumber, - PartnerID: r.PartnerID, - }, nil -} diff --git a/internal/handlers/request/member.go b/internal/handlers/request/member.go deleted file mode 100644 index ee3198e..0000000 --- a/internal/handlers/request/member.go +++ /dev/null @@ -1,36 +0,0 @@ -package request - -import ( - "time" -) - -type InitiateRegistrationRequest struct { - Name string `json:"name" validate:"required"` - Email string `json:"email" validate:"required,email"` - Phone string `json:"phone" validate:"required"` - BirthDate string `json:"birthDate" validate:"required"` - Password string `json:"password" validate:"required"` - ConfirmPassword string `json:"confirmPassword" validate:"required"` -} - -func (i *InitiateRegistrationRequest) GetBirthdate() (time.Time, error) { - parsedDate, err := time.Parse("02-01-2006", i.BirthDate) - if err != nil { - return time.Time{}, err - } - return parsedDate, nil -} - -type VerifyOTPRequest struct { - Token string `json:"token" validate:"required"` - OTP string `json:"otp" validate:"required"` -} - -type ResendOTPRequest struct { - Token string `json:"token" validate:"required"` -} - -type CheckCustomerRequest struct { - Email string `json:"email"` - Phone string `json:"phone"` -} diff --git a/internal/handlers/request/midtrans.go b/internal/handlers/request/midtrans.go deleted file mode 100644 index 17eaa11..0000000 --- a/internal/handlers/request/midtrans.go +++ /dev/null @@ -1,58 +0,0 @@ -package request - -import "enaklo-pos-be/internal/entity" - -type MidtransCallbackRequest struct { - VANumbers []VANumber `json:"va_numbers"` - TransactionTime string `json:"transaction_time"` - TransactionStatus string `json:"transaction_status"` - TransactionID string `json:"transaction_id"` - StatusMessage string `json:"status_message"` - StatusCode string `json:"status_code"` - SignatureKey string `json:"signature_key"` - SettlementTime string `json:"settlement_time"` - PaymentType string `json:"payment_type"` - OrderID string `json:"order_id"` - MerchantID string `json:"merchant_id"` - GrossAmount string `json:"gross_amount"` - FraudStatus string `json:"fraud_status"` - ExpiryTime string `json:"expiry_time"` - Currency string `json:"currency"` -} - -type VANumber struct { - VANumber string `json:"va_number"` - Bank string `json:"bank"` -} - -type MidtransCallbackBank struct { - Bank string `json:"bank"` - VaNumber string `json:"va_number"` - BillerCode string `json:"biller_code"` -} - -func (m *MidtransCallbackRequest) ToEntity() *entity.CallbackRequest { - return &entity.CallbackRequest{ - TransactionID: m.OrderID, - TransactionStatus: m.TransactionStatus, - } -} - -type LinQuCallback struct { - Amount int `json:"amount"` - SerialNumber string `json:"serialnumber"` - Type string `json:"type"` - PaymentReff int64 `json:"payment_reff"` - VaCode string `json:"va_code"` - PartnerReff string `json:"partner_reff"` - PartnerReff2 string `json:"partner_reff2"` - AdditionalFee int `json:"additionalfee"` - Balance int `json:"balance"` - CreditBalance int `json:"credit_balance"` - TransactionTime string `json:"transaction_time"` - VaNumber string `json:"va_number"` - CustomerName string `json:"customer_name"` - Username string `json:"username"` - Status string `json:"status"` - Signature string `json:"signature"` -} diff --git a/internal/handlers/request/order.go b/internal/handlers/request/order.go deleted file mode 100644 index 1eabf33..0000000 --- a/internal/handlers/request/order.go +++ /dev/null @@ -1,178 +0,0 @@ -package request - -import ( - "enaklo-pos-be/internal/common/mycontext" - "enaklo-pos-be/internal/constants/transaction" - "enaklo-pos-be/internal/entity" -) - -type Order struct { - CustomerName string `json:"customer_name"` - CustomerPhone string `json:"customer_phone"` - CustomerEmail string `json:"customer_email"` - PaymentMethod string `json:"payment_method"` - PaymentProvider string `json:"payment_provider"` - TableNumber string `json:"table_number"` - OrderItems []OrderItem `json:"order_items"` - PartnerID int64 `json:"partner_id"` -} - -type OrderCustomer struct { - CustomerName string `json:"customer_name" validate:"required"` - CustomerPhone string `json:"customer_phone"` - CustomerEmail string `json:"customer_email"` - TableNumber string `json:"table_number" validate:"required"` - OrderItems []OrderItem `json:"order_items" validate:"required"` - PartnerID int64 `json:"partner_id" validate:"required"` -} - -type CustomerOrder struct { - PartnerID int64 `json:"partner_id" validate:"required"` - PaymentMethod transaction.PaymentMethod `json:"payment_method" validate:"required,oneof=VA"` - OrderItems []OrderItem `json:"order_items" validate:"required,min=1"` - VisitDate string `json:"visit_date" validate:"required"` - BankCode string `json:"bank_code"` -} - -func (o *CustomerOrder) ToEntity(createdBy int64) *entity.OrderRequest { - orderItems := make([]entity.OrderItemRequest, len(o.OrderItems)) - for i, item := range o.OrderItems { - orderItems[i] = entity.OrderItemRequest{ - ProductID: item.ProductID, - Quantity: item.Quantity, - } - } - - return &entity.OrderRequest{ - PartnerID: o.PartnerID, - PaymentMethod: string(o.PaymentMethod), - OrderItems: orderItems, - CreatedBy: createdBy, - Source: "ONLINE", - } -} - -type OrderParam struct { - PaymentType string `form:"payment_type" json:"payment_type" example:"CASH"` - StartDate string `form:"start_date" json:"start_date"` - EndDate string `form:"end_date" json:"end_date"` - Status string `form:"status" json:"status"` - Limit int `form:"limit" json:"limit" example:"10"` - Offset int `form:"offset" json:"offset" example:"0"` - Period string `form:"period" json:"period" example:"1d,7d,1m"` - Source string `form:"source" json:"source" example:"tes"` -} - -type Checkin struct { - QRCode string `json:"qr_code" validate:"required"` -} - -type CheckinExecute struct { - Token string `json:"token" validate:"required"` -} - -func (o *OrderParam) ToOrderEntity(ctx mycontext.Context) entity.OrderSearch { - if o.Limit == 0 { - o.Limit = 10 - } - - return entity.OrderSearch{ - PartnerID: ctx.GetPartnerID(), - SiteID: ctx.GetSiteID(), - IsAdmin: ctx.IsAdmin(), - PaymentType: o.PaymentType, - Limit: o.Limit, - Offset: o.Offset, - StartDate: o.StartDate, - EndDate: o.EndDate, - Status: o.Status, - Period: o.Period, - Source: o.Source, - } -} - -type OrderItem struct { - ProductID int64 `json:"product_id" validate:"required"` - Quantity int `json:"quantity" validate:"required"` - Description string `json:"description"` - Notes string `json:"notes"` -} - -func (o *Order) ToEntity(partnerID, createdBy int64) *entity.OrderRequest { - orderItems := make([]entity.OrderItemRequest, len(o.OrderItems)) - for i, item := range o.OrderItems { - orderItems[i] = entity.OrderItemRequest{ - ProductID: item.ProductID, - Quantity: item.Quantity, - } - } - - return &entity.OrderRequest{ - PartnerID: partnerID, - PaymentMethod: o.PaymentMethod, - OrderItems: orderItems, - CreatedBy: createdBy, - Source: "POS", - } -} - -type Execute struct { - PartnerID int64 `json:"partner_id" validate:"required"` - Token string `json:"token"` -} - -func (e Execute) ToOrderExecuteRequest(createdBy int64) *entity.OrderExecuteRequest { - return &entity.OrderExecuteRequest{ - CreatedBy: createdBy, - PartnerID: e.PartnerID, - Token: e.Token, - } -} - -type OrderParamCustomer struct { - ID int64 `form:"id" json:"id" example:"10"` - ReferenceID string `form:"reference_id" json:"reference_id" example:"10"` - Limit int `form:"limit" json:"limit" example:"10"` - Offset int `form:"offset" json:"offset" example:"0"` -} - -func (o *OrderParamCustomer) ToOrderEntity(ctx mycontext.Context) entity.OrderSearch { - if o.Limit == 0 { - o.Limit = 10 - } - - return entity.OrderSearch{ - PartnerID: ctx.GetPartnerID(), - SiteID: ctx.GetSiteID(), - IsAdmin: ctx.IsAdmin(), - Limit: o.Limit, - Offset: o.Offset, - CreatedBy: ctx.RequestedBy(), - IsCustomer: true, - } -} - -type OrderPrintDetail struct { - ID int64 `form:"id" json:"id" example:"10"` -} - -func (o *OrderCustomer) ToEntity(partnerID, createdBy int64) *entity.OrderRequest { - orderItems := make([]entity.OrderItemRequest, len(o.OrderItems)) - for i, item := range o.OrderItems { - orderItems[i] = entity.OrderItemRequest{ - ProductID: item.ProductID, - Quantity: item.Quantity, - Description: item.Description, - Notes: item.Notes, - } - } - - return &entity.OrderRequest{ - PartnerID: partnerID, - OrderItems: orderItems, - CreatedBy: createdBy, - Source: "ONLINE_ORDER", - CustomerName: o.CustomerName, - TableNumber: o.TableNumber, - } -} diff --git a/internal/handlers/request/partner.go b/internal/handlers/request/partner.go deleted file mode 100644 index cac6b4c..0000000 --- a/internal/handlers/request/partner.go +++ /dev/null @@ -1,99 +0,0 @@ -package request - -import ( - "enaklo-pos-be/internal/common/mycontext" - "enaklo-pos-be/internal/entity" -) - -type PartnerParam struct { - Search string `form:"search" json:"search" example:"Ketua Umum"` - Name string `form:"name" json:"name" example:"Ketua Umum"` - Limit int `form:"limit" json:"limit" example:"10"` - Offset int `form:"offset" json:"offset" example:"0"` -} - -func (p *PartnerParam) ToEntity(ctx mycontext.Context) entity.PartnerSearch { - return entity.PartnerSearch{ - Search: p.Search, - PartnerID: ctx.GetPartnerID(), - Name: p.Name, - Limit: p.Limit, - Offset: p.Offset, - } -} - -type Partner struct { - ID int64 `json:"id"` - Name string `json:"name"` - Address string `json:"address"` - Status string `json:"status"` - Email string `json:"email"` - PhoneNumber string `json:"phone_number"` - Password string `json:"password"` - AdminUserID int64 `json:"admin_user_id"` - AdminName string `json:"admin_name"` - BankName string `json:"bank_name"` - BankAccountNumber string `json:"bank_account_number"` - BankAccountHolderName string `json:"bank_account_holder_name"` - NIK string `json:"nik"` - Logo string `json:"logo"` -} - -type CreatePartnerRequest struct { - Name string `json:"name" validate:"required"` - Email string `json:"email" validate:"required"` - Address string `json:"address" validate:"required"` - FullName string `json:"full_name" validate:"required"` - Password string `json:"password" validate:"required"` - NIK string `json:"nik"` - PhoneNumber string `json:"phone_number" validate:"required"` - BankName string `json:"bank_name" validate:"required"` - BankAccountNumber string `json:"bank_account_number" validate:"required"` - BankAccountHolderName string `json:"bank_account_holder_name" validate:"required"` - Status string `json:"status"` - Logo string `json:"logo"` -} - -func (e *CreatePartnerRequest) ToEntity() *entity.CreatePartnerRequest { - return &entity.CreatePartnerRequest{ - Name: e.Name, - Address: e.Address, - FullName: e.FullName, - Email: e.Email, - Password: e.Password, - NIK: e.NIK, - PhoneNumber: e.PhoneNumber, - BankName: e.BankName, - BankAccountNumber: e.BankAccountNumber, - BankAccountHolderName: e.BankAccountHolderName, - Status: e.Status, - Logo: e.Logo, - } -} - -func (e *Partner) ToEntity() *entity.Partner { - return &entity.Partner{ - Name: e.Name, - Address: e.Address, - Status: e.Status, - } -} - -func (e *Partner) ToEntityUpdate(partnerID int64) *entity.PartnerUpdate { - return &entity.PartnerUpdate{ - ID: partnerID, - Name: e.Name, - Email: e.Email, - Address: e.Address, - Status: e.Status, - PhoneNumber: e.PhoneNumber, - BankName: e.BankName, - BankAccountNumber: e.BankAccountNumber, - BankAccountHolderName: e.BankAccountHolderName, - NIK: e.NIK, - AdminName: e.AdminName, - Password: e.Password, - AdminUserID: e.AdminUserID, - Logo: e.Logo, - } -} diff --git a/internal/handlers/request/product.go b/internal/handlers/request/product.go deleted file mode 100644 index 22a79f1..0000000 --- a/internal/handlers/request/product.go +++ /dev/null @@ -1,57 +0,0 @@ -package request - -import ( - "enaklo-pos-be/internal/constants/product" - "enaklo-pos-be/internal/entity" -) - -type ProductParam struct { - Search string `form:"search" json:"search" example:"Nasi Goreng"` - Name string `form:"name" json:"name" example:"Nasi Goreng"` - Type product.ProductType `form:"type" json:"type" example:"FOOD/BEVERAGE"` - BranchID int64 `form:"branch_id" json:"branch_id" example:"1"` - Available product.ProductStock `form:"available" json:"available" example:"1" example:"AVAILABLE/UNAVAILABLE"` - Limit int `form:"limit" json:"limit" example:"10"` - Offset int `form:"offset" json:"offset" example:"0"` - CategoryID int64 `form:"category_id" json:"category_id" example:"1"` -} - -func (p *ProductParam) ToEntity(partnerID int64) entity.ProductSearch { - return entity.ProductSearch{ - Search: p.Search, - Name: p.Name, - Type: p.Type, - PartnerID: partnerID, - Available: p.Available, - Limit: p.Limit, - Offset: p.Offset, - CategoryID: p.CategoryID, - } -} - -type Product struct { - ID int64 `json:"id,omitempty"` - PartnerID int64 `json:"partner_id"` - SiteID int64 `json:"site_id"` - Name string `json:"name" validate:"required"` - Type string `json:"type"` - Price float64 `json:"price" validate:"required"` - Status string `json:"status"` - Description string `json:"description"` - Stock int64 `json:"stock"` - Image string `json:"image"` - CategoryID int64 `json:"category_id"` -} - -func (e *Product) ToEntity() *entity.Product { - return &entity.Product{ - Name: e.Name, - Type: e.Type, - Price: e.Price, - Status: e.Status, - Description: e.Description, - PartnerID: e.PartnerID, - Image: e.Image, - CategoryID: &e.CategoryID, - } -} diff --git a/internal/handlers/request/query.go b/internal/handlers/request/query.go deleted file mode 100644 index abfcd28..0000000 --- a/internal/handlers/request/query.go +++ /dev/null @@ -1,126 +0,0 @@ -package request - -import ( - "enaklo-pos-be/internal/entity" - "github.com/gin-gonic/gin" - "github.com/pkg/errors" - "strconv" - "time" -) - -type QueryParser struct { - defaultLimit int - maxLimit int -} - -func NewQueryParser() *QueryParser { - return &QueryParser{ - defaultLimit: 20, - maxLimit: 100, - } -} - -func (p *QueryParser) ParseSearchRequest(c *gin.Context) (*entity.SearchRequest, error) { - req := &entity.SearchRequest{} - - if status := c.Query("status"); status != "" { - req.Status = status - } - - limit, err := p.parseLimit(c.Query("limit")) - if err != nil { - return nil, errors.Wrap(err, "invalid limit parameter") - } - req.Limit = limit - - offset, err := p.parseOffset(c.Query("offset")) - if err != nil { - return nil, errors.Wrap(err, "invalid offset parameter") - } - req.Offset = offset - - if err := p.parseDateRange(c, req); err != nil { - return nil, errors.Wrap(err, "invalid date parameters") - } - - return req, nil -} - -func (p *QueryParser) parseLimit(limitStr string) (int, error) { - if limitStr == "" { - return p.defaultLimit, nil - } - - limit, err := strconv.Atoi(limitStr) - if err != nil { - return 0, errors.New("limit must be a valid integer") - } - - if limit <= 0 { - return p.defaultLimit, nil - } - - if limit > p.maxLimit { - limit = p.maxLimit - } - - return limit, nil -} - -func (p *QueryParser) parseOffset(offsetStr string) (int, error) { - if offsetStr == "" { - return 0, nil - } - - offset, err := strconv.Atoi(offsetStr) - if err != nil { - return 0, errors.New("offset must be a valid integer") - } - - if offset < 0 { - return 0, nil - } - - return offset, nil -} - -func (p *QueryParser) parseDateRange(c *gin.Context, req *entity.SearchRequest) error { - if startDateStr := c.Query("start_date"); startDateStr != "" { - startDate, err := p.parseDate(startDateStr) - if err != nil { - return errors.Wrap(err, "invalid start_date format") - } - req.Start = startDate - } - - if endDateStr := c.Query("end_date"); endDateStr != "" { - endDate, err := p.parseDate(endDateStr) - if err != nil { - return errors.Wrap(err, "invalid end_date format") - } - req.End = endDate - } - - if !req.Start.IsZero() && !req.End.IsZero() && req.Start.After(req.End) { - return errors.New("start_date cannot be after end_date") - } - - return nil -} - -func (p *QueryParser) parseDate(dateStr string) (time.Time, error) { - formats := []string{ - time.RFC3339, - "2006-01-02T15:04:05", - "2006-01-02", - "2006-01-02 15:04:05", - } - - for _, format := range formats { - if date, err := time.Parse(format, dateStr); err == nil { - return date, nil - } - } - - return time.Time{}, errors.New("unsupported date format") -} diff --git a/internal/handlers/request/site.go b/internal/handlers/request/site.go deleted file mode 100644 index fe1368f..0000000 --- a/internal/handlers/request/site.go +++ /dev/null @@ -1,90 +0,0 @@ -package request - -import ( - "enaklo-pos-be/internal/common/mycontext" - "enaklo-pos-be/internal/entity" -) - -type Site struct { - ID int64 `json:"id"` - Name string `json:"name" validate:"required"` - PartnerID *int64 `json:"partner_id"` - Image string `json:"image"` - Address string `json:"address"` - LocationLink string `json:"location_link"` - Description string `json:"description"` - Highlight string `json:"highlight"` - ContactPerson string `json:"contact_person"` - TnC string `json:"tnc"` - AdditionalInfo string `json:"additional_info"` - Status string `json:"status"` - IsSeasonTicket bool `json:"is_season_ticket"` - IsDiscountActive bool `json:"is_discount_active"` - Products []Product `json:"products"` - Region string `json:"region"` - Regency string `json:"regency"` - Lat float64 `json:"lat"` - Long float64 `json:"long"` -} - -func (r *Site) ToEntity(createdBy int64) *entity.Site { - var products []entity.Product - for _, p := range r.Products { - products = append(products, entity.Product{ - ID: p.ID, - PartnerID: *r.PartnerID, - Name: p.Name, - Type: p.Type, - Price: p.Price, - Status: p.Status, - Description: p.Description, - CreatedBy: createdBy, - }) - } - - return &entity.Site{ - ID: r.ID, - Name: r.Name, - PartnerID: *r.PartnerID, - Image: r.Image, - Address: r.Address, - LocationLink: r.LocationLink, - Description: r.Description, - Highlight: r.Highlight, - ContactPerson: r.ContactPerson, - TnC: r.TnC, - AdditionalInfo: r.AdditionalInfo, - Status: r.Status, - IsSeasonTicket: r.IsSeasonTicket, - IsDiscountActive: r.IsDiscountActive, - Products: products, - Region: r.Region, - Regency: r.Regency, - Latitude: &r.Lat, - Longitude: &r.Long, - } -} - -type SiteParam struct { - Search string `form:"search"` - PartnerID *int64 `form:"partner_id"` - Name string `form:"name"` - Limit int `form:"limit,default=10"` - Offset int `form:"offset,default=0"` -} - -func (r *SiteParam) ToEntity(ctx mycontext.Context, partnerID *int64, siteID *int64) entity.SiteSearch { - if partnerID == nil { - partnerID = r.PartnerID - } - - return entity.SiteSearch{ - PartnerID: partnerID, - IsAdmin: ctx.IsAdmin(), - SiteID: siteID, - Search: r.Search, - Name: r.Name, - Limit: r.Limit, - Offset: r.Offset, - } -} diff --git a/internal/handlers/request/studio.go b/internal/handlers/request/studio.go deleted file mode 100644 index b83cd1f..0000000 --- a/internal/handlers/request/studio.go +++ /dev/null @@ -1,53 +0,0 @@ -package request - -import ( - "enaklo-pos-be/internal/constants/studio" - "enaklo-pos-be/internal/entity" - "encoding/json" -) - -type StudioParam struct { - Id string `form:"id" json:"id" example:"1"` - Name string `form:"name" json:"name" example:"Studio A"` - Status studio.StudioStatus `form:"status" json:"status" example:"Active"` - BranchId int64 `form:"branch_id" json:"branch_id" example:"1"` - Limit int `form:"limit" json:"limit" example:"10"` - Offset int `form:"offset" json:"offset" example:"0"` -} - -func (p *StudioParam) ToEntity() entity.StudioSearch { - return entity.StudioSearch{ - Name: p.Name, - Status: p.Status, - BranchId: p.BranchId, - Limit: p.Limit, - Offset: p.Offset, - } -} - -type Studio struct { - Name string `json:"name" validate:"required"` - BranchId int64 `json:"branch_id" validate:"required"` - Status studio.StudioStatus `json:"status"` - Price float64 `json:"price" validate:"required"` - Metadata map[string]interface{} `json:"metadata"` -} - -func (e *Studio) ToEntity() *entity.Studio { - studioEntity := &entity.Studio{ - BranchId: e.BranchId, - Name: e.Name, - Status: e.Status, - Price: e.Price, - } - - if e.Metadata != nil { - jsonData, err := json.Marshal(e.Metadata) - if err != nil { - //TODO @taufanvps - } - studioEntity.Metadata = jsonData - } - - return studioEntity -} diff --git a/internal/handlers/request/transaction.go b/internal/handlers/request/transaction.go deleted file mode 100644 index 3013e38..0000000 --- a/internal/handlers/request/transaction.go +++ /dev/null @@ -1,64 +0,0 @@ -package request - -import ( - "enaklo-pos-be/internal/common/mycontext" - "enaklo-pos-be/internal/constants/transaction" - "enaklo-pos-be/internal/entity" - "github.com/go-playground/validator/v10" -) - -type Transaction struct { - PaymentMethod transaction.PaymentMethod -} - -type TransactionSearch struct { - Id string `form:"id" json:"id" example:"1"` - Date string `form:"date" json:"date" example:"1"` - Limit int `form:"limit,default=10"` - Offset int `form:"offset,default=0"` - Status string `form:"status,default="` - Type string `form:"type,default="` -} - -type ApprovalRequest struct { - Status string `json:"status" validate:"required,approvalStatus"` - TransactionID string `json:"transaction_id" validate:"required"` -} - -func (a *ApprovalRequest) ToEntity() *entity.TransactionApproval { - return &entity.TransactionApproval{ - Status: a.Status, - TransactionID: a.TransactionID, - } -} - -func approvalStatus(fl validator.FieldLevel) bool { - status := fl.Field().String() - return status == "APPROVE" || status == "REJECT" -} - -func (a *ApprovalRequest) Validate() error { - validate := validator.New() - err := validate.RegisterValidation("approvalStatus", approvalStatus) - if err != nil { - return err - } - - if err := validate.Struct(a); err != nil { - return err - } - - return nil -} - -func (t *TransactionSearch) ToEntity(ctx mycontext.Context) entity.TransactionSearch { - return entity.TransactionSearch{ - PartnerID: ctx.GetPartnerID(), - SiteID: ctx.GetSiteID(), - Type: t.Type, - Status: t.Status, - Limit: t.Limit, - Offset: t.Offset, - Date: t.Date, - } -} diff --git a/internal/handlers/request/user.go b/internal/handlers/request/user.go deleted file mode 100644 index 06a5d1a..0000000 --- a/internal/handlers/request/user.go +++ /dev/null @@ -1,178 +0,0 @@ -package request - -import ( - "enaklo-pos-be/internal/common/mycontext" - "enaklo-pos-be/internal/constants/role" - "enaklo-pos-be/internal/entity" - "math/rand" - "strings" - "time" - - "github.com/go-playground/validator/v10" -) - -type User struct { - Name string `json:"name" validate:"required"` - Email string `json:"email" validate:"required,email"` - Password string `json:"password"` - PartnerID *int64 `json:"partner_id"` - SiteID *int64 `json:"site_id"` - RoleID role.Role `json:"role_id" validate:"required"` - NIK string `json:"nik"` - UserType string `json:"user_type"` - PhoneNumber string `json:"phone_number"` -} - -func (e *User) Validate() error { - validate := validator.New() - if err := validate.Struct(e); err != nil { - return err - } - - return nil -} - -func RandomEmail() string { - const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" - const length = 8 - - seededRand := rand.New(rand.NewSource(time.Now().UnixNano())) - randomString := make([]byte, length) - - for i := range randomString { - randomString[i] = charset[seededRand.Intn(len(charset))] - } - - return string(randomString) + "@example.com" -} - -func (u *User) ToEntity(ctx mycontext.Context, userType string) *entity.User { - if !ctx.IsAdmin() { - u.PartnerID = ctx.GetPartnerID() - } - - if u.Email == "" { - u.Email = RandomEmail() - } - - return &entity.User{ - Name: u.Name, - Email: strings.ToLower(u.Email), - Password: u.Password, - RoleID: role.Role(u.RoleID), - PartnerID: u.PartnerID, - SiteID: u.SiteID, - NIK: u.NIK, - UserType: userType, - PhoneNumber: u.PhoneNumber, - } -} - -type Customer struct { - Name string `json:"name" binding:"required"` - Email string `json:"email" binding:"required,email"` - PhoneNumber string `json:"phone_number" binding:"required"` - RoleID int64 `json:"role_id" binding:"required"` - SiteID *int64 `json:"site_id" binding:"required"` -} - -func (c *Customer) ToEntity(ctx mycontext.Context, userType string) entity.Customer { - return entity.Customer{ - Name: c.Name, - Email: c.Email, - PhoneNumber: c.PhoneNumber, - SiteID: c.SiteID, - PartnerID: ctx.GetPartnerID(), - } -} - -type UserParam struct { - Search string `form:"search" json:"search" example:"admin,branch1"` - Name string `form:"name" json:"name" example:"Admin 1"` - RoleID int64 `form:"role_id" json:"role_id" example:"1"` - PartnerID int64 `form:"partner_id" json:"partner_id" example:"1"` - SiteID int64 `form:"site_id" json:"site_id" example:"1"` - Limit int `form:"limit,default=10" json:"limit" example:"10"` - Offset int `form:"offset,default=0" json:"offset" example:"0"` -} - -func (p *UserParam) ToEntity(ctx mycontext.Context) entity.UserSearch { - partnerID := p.PartnerID - siteID := p.SiteID - - if !ctx.IsAdmin() { - partnerID = *ctx.GetPartnerID() - } - - if ctx.GetSiteID() != nil { - siteID = *ctx.GetSiteID() - } - - return entity.UserSearch{ - Search: p.Search, - Name: p.Name, - RoleID: p.RoleID, - PartnerID: partnerID, - SiteID: siteID, - Limit: p.Limit, - Offset: p.Offset, - } -} - -type CustomerParam struct { - Search string `form:"search" json:"search" example:"admin,branch1"` - Name string `form:"name" json:"name" example:"Admin 1"` - RoleID int64 `form:"role_id" json:"role_id" example:"1"` - PartnerID int64 `form:"partner_id" json:"partner_id" example:"1"` - SiteID int64 `form:"site_id" json:"site_id" example:"1"` - Limit int `form:"limit,default=10" json:"limit" example:"10"` - Offset int `form:"offset,default=0" json:"offset" example:"0"` -} - -func (p *CustomerParam) ToEntity(ctx mycontext.Context) entity.CustomerSearch { - partnerID := p.PartnerID - siteID := p.SiteID - - if !ctx.IsAdmin() { - partnerID = *ctx.GetPartnerID() - } - - if ctx.GetSiteID() != nil { - siteID = *ctx.GetSiteID() - } - - return entity.CustomerSearch{ - Search: p.Search, - Name: p.Name, - RoleID: p.RoleID, - PartnerID: partnerID, - SiteID: siteID, - Limit: p.Limit, - Offset: p.Offset, - } -} - -type UserRegister struct { - Name string `json:"name" validate:"required" binding:"required"` - Email string `json:"email" validate:"required" binding:"required"` - PhoneNumber string `json:"phone_number" validate:"required" binding:"required"` - Password string `json:"password" validate:"required" binding:"required"` -} - -func (e *UserRegister) Validate() error { - validate := validator.New() - if err := validate.Struct(e); err != nil { - return err - } - - return nil -} - -func (u *UserRegister) ToEntity() *entity.User { - return &entity.User{ - Name: u.Name, - Email: strings.ToLower(u.Email), - PhoneNumber: u.PhoneNumber, - Password: u.Password, - } -} diff --git a/internal/handlers/request/validator.go b/internal/handlers/request/validator.go deleted file mode 100644 index 844a711..0000000 --- a/internal/handlers/request/validator.go +++ /dev/null @@ -1,50 +0,0 @@ -package request - -import ( - errors2 "errors" - "github.com/go-playground/validator/v10" - "reflect" -) - -var validate *validator.Validate - -func ValidateAndHandleError(req interface{}) error { - validate = validator.New() - - if err := validate.Struct(req); err != nil { - formattedError := formatValidationError(err) - return errors2.New(formattedError) - } - return nil -} - -func formatValidationError(err error) string { - if _, ok := err.(*validator.InvalidValidationError); ok { - return err.Error() - } - - var errorMessage string - for _, err := range err.(validator.ValidationErrors) { - switch err.Tag() { - case "required": - errorMessage += "The field '" + err.Field() + "' is required." - case "min": - if err.Kind() == reflect.Slice { - errorMessage += "The field '" + err.Field() + "' must contain at least " + err.Param() + " items." - } else { - errorMessage += "The field '" + err.Field() + "' must be at least " + err.Param() + "." - } - case "oneof": - errorMessage += "The field '" + err.Field() + "' must be one of [" + err.Param() + "]." - case "email": - errorMessage += "The field '" + err.Field() + "' must be a valid email address." - case "len": - errorMessage += "The field '" + err.Field() + "' must be exactly " + err.Param() + " characters long." - default: - errorMessage += "The field '" + err.Field() + "' is invalid." - } - errorMessage += " " - } - - return errorMessage -} diff --git a/internal/handlers/request/validator/request_validator.go b/internal/handlers/request/validator/request_validator.go deleted file mode 100644 index 82c41c1..0000000 --- a/internal/handlers/request/validator/request_validator.go +++ /dev/null @@ -1,43 +0,0 @@ -package validator - -import ( - "enaklo-pos-be/internal/entity" - "github.com/pkg/errors" - "time" -) - -type RequestValidator struct{} - -func NewRequestValidator() *RequestValidator { - return &RequestValidator{} -} - -func (v *RequestValidator) ValidateSearchRequest(req *entity.SearchRequest) error { - if req.Status != "" { - validStatuses := []string{"pending", "confirmed", "processing", "completed", "cancelled"} - if !v.isValidStatus(req.Status, validStatuses) { - return errors.New("invalid status value") - } - } - - if !req.Start.IsZero() && !req.End.IsZero() { - if req.Start.After(req.End) { - return errors.New("start date cannot be after end date") - } - - if req.End.Sub(req.Start) > 365*24*time.Hour { - return errors.New("date range cannot exceed 1 year") - } - } - - return nil -} - -func (v *RequestValidator) isValidStatus(status string, validStatuses []string) bool { - for _, validStatus := range validStatuses { - if status == validStatus { - return true - } - } - return false -} diff --git a/internal/handlers/response/auth.go b/internal/handlers/response/auth.go deleted file mode 100644 index 7e3eabc..0000000 --- a/internal/handlers/response/auth.go +++ /dev/null @@ -1,28 +0,0 @@ -package response - -type LoginResponse struct { - Token string `json:"token"` - Name string `json:"name"` - Role Role `json:"role"` - Partner *Partner `json:"partner"` - Site *SiteName `json:"site,omitempty"` - ResetPassword bool `json:"reset_password"` - PartnerLicense *PartnerLicense `json:"partner_license,omitempty"` -} - -type LoginResponseCustoemr struct { - ID int64 `json:"id"` - Token string `json:"token"` - Name string `json:"name"` - ResetPassword bool `json:"reset_password"` -} - -type Role struct { - ID int64 `json:"id"` - Role string `json:"role_name"` -} - -type PartnerLicense struct { - DaysToExpire int64 `json:"days_to_expire"` - Status string `json:"status"` -} diff --git a/internal/handlers/response/base_response.go b/internal/handlers/response/base_response.go deleted file mode 100644 index baeeb28..0000000 --- a/internal/handlers/response/base_response.go +++ /dev/null @@ -1,13 +0,0 @@ -package response - -type BaseResponse struct { - Success bool `json:"success"` - Code string `json:"response_code,omitempty"` - Status int `json:"-"` - Message string `json:"message,omitempty"` - ErrorMessage string `json:"error_message,omitempty"` - ErrorDetail interface{} `json:"error_detail,omitempty"` - Data interface{} `json:"data,omitempty"` - PagingMeta *PagingMeta `json:"meta,omitempty"` - Response string `json:"response,omitempty"` -} diff --git a/internal/handlers/response/branch.go b/internal/handlers/response/branch.go deleted file mode 100644 index 6130b6a..0000000 --- a/internal/handlers/response/branch.go +++ /dev/null @@ -1,20 +0,0 @@ -package response - -type Balance struct { - PartnerID int64 `json:"partner_id"` - Balance float64 `json:"balance"` - AuthBalance float64 `json:"auth_balance"` -} - -type BalanceInquiryResponse struct { - PartnerID int64 `json:"partner_id"` - Total int64 `json:"total"` - Amount int64 `json:"amount"` - Fee int64 `json:"fee"` - Token string `json:"token"` -} - -type BalanceExecuteResponse struct { - TransactionID string `json:"transaction_id"` - Status string `json:"status"` -} diff --git a/internal/handlers/response/cashier.go b/internal/handlers/response/cashier.go deleted file mode 100644 index 575dcd4..0000000 --- a/internal/handlers/response/cashier.go +++ /dev/null @@ -1,64 +0,0 @@ -package response - -import ( - "enaklo-pos-be/internal/entity" - "time" -) - -type CashierSessionResponse struct { - ID int64 `json:"id"` - PartnerID int64 `json:"partner_id"` - CashierID int64 `json:"cashier_id"` - OpenedAt time.Time `json:"opened_at"` - ClosedAt *time.Time `json:"closed_at,omitempty"` - OpeningAmount float64 `json:"opening_amount"` - ClosingAmount *float64 `json:"closing_amount,omitempty"` - Status string `json:"status"` -} - -type PaymentSummaryResponse struct { - PaymentType string `json:"payment_type"` - PaymentProvider string `json:"payment_provider"` - TotalAmount float64 `json:"total_amount"` -} - -type CashierSessionReportResponse struct { - SessionID int64 `json:"session_id"` - ExpectedAmount float64 `json:"expected_amount"` - ClosingAmount float64 `json:"closing_amount"` - Payments []PaymentSummaryResponse `json:"payments"` -} - -func MapToCashierSessionResponse(e *entity.CashierSession) *CashierSessionResponse { - if e == nil { - return nil - } - - return &CashierSessionResponse{ - ID: e.ID, - PartnerID: e.PartnerID, - CashierID: e.CashierID, - OpenedAt: e.OpenedAt, - ClosedAt: e.ClosedAt, - OpeningAmount: e.OpeningAmount, - ClosingAmount: e.ClosingAmount, - Status: e.Status, - } -} - -func MapToCashierSessionReport(e *entity.CashierSessionReport) *CashierSessionReportResponse { - payments := make([]PaymentSummaryResponse, len(e.Payments)) - for i, p := range e.Payments { - payments[i] = PaymentSummaryResponse{ - PaymentType: p.PaymentType, - PaymentProvider: p.PaymentProvider, - TotalAmount: p.TotalAmount, - } - } - return &CashierSessionReportResponse{ - SessionID: e.SessionID, - ExpectedAmount: e.ExpectedAmount, - ClosingAmount: e.ClosingAmount, - Payments: payments, - } -} diff --git a/internal/handlers/response/category.go b/internal/handlers/response/category.go deleted file mode 100644 index 48ee45c..0000000 --- a/internal/handlers/response/category.go +++ /dev/null @@ -1,29 +0,0 @@ -package response - -import "enaklo-pos-be/internal/entity" - -type CategoryResponse struct { - ID int64 `json:"id"` - Name string `json:"name"` - PartnerID int64 `json:"partner_id"` -} - -func MapToCategoryResponse(cat *entity.Category) *CategoryResponse { - if cat == nil { - return nil - } - - return &CategoryResponse{ - ID: cat.ID, - Name: cat.Name, - PartnerID: cat.PartnerID, - } -} - -func MapToCategoryListResponse(cats []*entity.Category) []*CategoryResponse { - result := make([]*CategoryResponse, len(cats)) - for i, c := range cats { - result[i] = MapToCategoryResponse(c) - } - return result -} diff --git a/internal/handlers/response/customer.go b/internal/handlers/response/customer.go deleted file mode 100644 index 76c9f1e..0000000 --- a/internal/handlers/response/customer.go +++ /dev/null @@ -1,41 +0,0 @@ -package response - -import ( - "enaklo-pos-be/internal/entity" -) - -func MapToCustomerResponse(customer *entity.Customer) CustomerResponse { - if customer == nil { - return CustomerResponse{} - } - - return CustomerResponse{ - ID: customer.ID, - Name: customer.Name, - Email: customer.Email, - Phone: customer.Phone, - Points: customer.Points, - CustomerID: customer.CustomerID, - CreatedAt: customer.CreatedAt.Format("2006-01-02"), - BirthDate: customer.BirthDate.Format("2006-01-02"), - } -} - -func MapToCustomerListResponse(customers *entity.MemberList) []CustomerResponse { - if customers == nil { - return []CustomerResponse{} - } - - responseList := []CustomerResponse{} - for _, customer := range *customers { - responseList = append(responseList, MapToCustomerResponse(customer)) - } - - return responseList -} - -type CustomerRegistrationResp struct { - EmailVerificationRequired bool `json:"email_verification_required"` - PhoneVerificationRequired bool `json:"phone_verification_required"` - VerificationID string `json:"verification_id"` -} diff --git a/internal/handlers/response/discovery.go b/internal/handlers/response/discovery.go deleted file mode 100644 index 636b8a4..0000000 --- a/internal/handlers/response/discovery.go +++ /dev/null @@ -1,83 +0,0 @@ -package response - -type ExploreResponse struct { - ExploreRegions []Region `json:"exploreRegions"` - ExploreDestinations []Destination `json:"exploreDestinations"` - MustVisit []MustVisit `json:"mustVisit"` -} - -type CurrentLocation struct { - City string `json:"city"` -} - -type Region struct { - Name string `json:"name"` -} - -type Destination struct { - Name string `json:"name"` - ImageURL string `json:"image_url"` -} - -type MustVisit struct { - SiteID int64 `json:"site_id"` - Name string `json:"name"` - Region string `json:"region"` - Rating float64 `json:"rating"` - ReviewCount int `json:"reviewCount"` - Price float64 `json:"price"` - ImageURL string `json:"imageUrl"` - Regency string `json:"regency"` -} - -type SearchResponse struct { - Offset int `json:"offset"` - Total int `json:"total"` - Limit int `json:"limit"` - Data []SiteSeach `json:"data"` -} - -type SiteSeach struct { - SiteID int64 `json:"site_id"` - Name string `json:"name"` - Region string `json:"region"` - Rating float64 `json:"rating"` - ReviewCount int `json:"reviewCount"` - Price float64 `json:"price"` - ImageURL string `json:"imageUrl"` - Regency string `json:"regency"` -} - -type SearchSiteByIDResponse struct { - ID int64 `json:"id"` - Name string `json:"name"` - PartnerID int64 `json:"partner_id"` - Image string `json:"image"` - Address string `json:"address"` - LocationLink string `json:"location_link"` - Description string `json:"description"` - Highlight string `json:"highlight"` - ContactPerson string `json:"contact_person"` - TnC string `json:"tn_c"` - AdditionalInfo string `json:"additional_info"` - Status string `json:"status"` - Region string `json:"region"` - Regency string `json:"regency"` -} - -type SearchProductSiteByIDResponse struct { - ID int64 `json:"id"` - - Name string `json:"name"` - Type string `json:"type"` - Price float64 `json:"price"` - IsWeekendTicket bool `json:"is_weekend_ticket"` - IsSeasonTicket bool `json:"is_season_ticket"` - Description string `json:"description"` - PartnerID int64 `json:"partner_id"` -} - -type SearchProductSiteResponse struct { - Product []SearchProductSiteByIDResponse `json:"product"` - PartnerID int64 `json:"partner_id"` -} diff --git a/internal/handlers/response/event.go b/internal/handlers/response/event.go deleted file mode 100644 index df85465..0000000 --- a/internal/handlers/response/event.go +++ /dev/null @@ -1,27 +0,0 @@ -package response - -type Event struct { - ID int64 `json:"id"` - Name string `json:"name"` - Status string `json:"status"` - Description string `json:"description"` - StartDate string `json:"start_date"` - EndDate string `json:"end_date"` - StartTime string `json:"start_time"` - EndTime string `json:"end_time"` - Location string `json:"location"` - Level string `json:"level"` - Included []string `json:"included"` - Price float64 `json:"price"` - Paid bool `json:"paid"` - LocationID *int64 `json:"location_id"` - CreatedAt string `json:"created_at"` - UpdatedAt string `json:"updated_at"` -} - -type EventList struct { - Events []Event `json:"events"` - Total int64 `json:"total"` - Limit int `json:"limit"` - Offset int `json:"offset"` -} diff --git a/internal/handlers/response/handler.go b/internal/handlers/response/handler.go deleted file mode 100644 index 00d751f..0000000 --- a/internal/handlers/response/handler.go +++ /dev/null @@ -1,43 +0,0 @@ -package response - -import ( - "github.com/gin-gonic/gin" - - "enaklo-pos-be/internal/common/errors" -) - -type response struct { - Status int `json:"status"` - Meta interface{} `json:"meta,omitempty"` - Message string `json:"message"` - Data interface{} `json:"data"` - Success bool `json:"success,omitempty"` -} - -func ErrorWrapper(c *gin.Context, err error) { - var customError errors.Error - customError = errors.ErrorInternalServer - - status := customError.MapErrorsToHTTPCode() - code := customError.MapErrorsToCode() - message := err.Error() - - if validErr, ok := err.(errors.Error); ok { - status = validErr.MapErrorsToHTTPCode() - code = validErr.MapErrorsToCode() - - if code.GetHTTPCode() != 0 { - status = code.GetHTTPCode() - } - - message = code.GetMessage() - } - - resp := BaseResponse{ - ErrorMessage: err.Error(), - Code: code.GetCode(), - Message: message, - } - - c.JSON(status, resp) -} diff --git a/internal/handlers/response/license.go b/internal/handlers/response/license.go deleted file mode 100644 index 555496a..0000000 --- a/internal/handlers/response/license.go +++ /dev/null @@ -1,72 +0,0 @@ -package response - -import ( - "enaklo-pos-be/internal/entity" -) - -type License struct { - ID string `json:"id"` - Name string `json:"name"` - StartDate string `json:"start_date"` - EndDate string `json:"end_date"` - RenewalDate string `json:"renewal_date,omitempty"` - SerialNumber string `json:"serial_number"` - PartnerID int64 `json:"partner_id"` - CreatedBy string `json:"created_by"` - UpdatedBy int64 `json:"updated_by"` - PartnerName string `json:"partner_name"` - Status string `json:"status"` - DaysToExpire int64 `json:"days_to_expire"` -} - -type LicenseList struct { - Licenses []License `json:"licenses"` - Total int64 `json:"total"` - Limit int `json:"limit"` - Offset int `json:"offset"` -} - -func (r *License) FromEntity(e *entity.License) { - r.ID = e.ID.String() - r.Name = e.Name - r.StartDate = e.StartDate.Format("2006-01-02") - r.EndDate = e.EndDate.Format("2006-01-02") - if e.RenewalDate != nil { - r.RenewalDate = e.RenewalDate.Format("2006-01-02") - } - r.SerialNumber = e.SerialNumber - r.PartnerID = e.PartnerID -} - -func FromEntityList(entities []*entity.License) []License { - licenses := make([]License, len(entities)) - for i, e := range entities { - licenses[i].FromEntity(e) - } - return licenses -} - -func FromEntityListAll(entities []*entity.LicenseGetAll) []License { - licenses := make([]License, len(entities)) - for i, e := range entities { - licenses[i].FromEntityGetAll(e) - } - return licenses -} - -func (r *License) FromEntityGetAll(e *entity.LicenseGetAll) { - r.ID = e.ID.String() - r.Name = e.Name - r.StartDate = e.StartDate.Format("2006-01-02") - r.EndDate = e.EndDate.Format("2006-01-02") - if e.RenewalDate != nil { - r.RenewalDate = e.RenewalDate.Format("2006-01-02") - } - r.SerialNumber = e.SerialNumber - r.PartnerID = e.PartnerID - r.CreatedBy = e.CreatedByName - r.UpdatedBy = e.UpdatedBy - r.PartnerName = e.PartnerName - r.Status = e.LicenseStatus - r.DaysToExpire = e.DaysToExpire -} diff --git a/internal/handlers/response/member.go b/internal/handlers/response/member.go deleted file mode 100644 index c0f2b55..0000000 --- a/internal/handlers/response/member.go +++ /dev/null @@ -1,111 +0,0 @@ -package response - -import ( - "enaklo-pos-be/internal/entity" - "time" -) - -type MemberRegistrationResponse struct { - Token string `json:"token"` - Status string `json:"status"` - ExpiresAt time.Time `json:"expires_at"` - Message string `json:"message"` -} - -type MemberVerificationResponse struct { - CustomerID int64 `json:"customer_id"` - Name string `json:"name"` - Email string `json:"email"` - Phone string `json:"phone"` - Points int `json:"points"` - Status string `json:"status"` -} - -type MemberRegistrationStatus struct { - Token string `json:"token"` - Status string `json:"status"` - ExpiresAt time.Time `json:"expires_at"` - IsExpired bool `json:"is_expired"` - CreatedAt time.Time `json:"created_at"` -} - -type ResendOTPResponse struct { - Token string `json:"token"` - ExpiresAt time.Time `json:"expires_at"` - Message string `json:"message"` -} - -type CustomerCheckResponse struct { - Exists bool `json:"exists"` - Customer *CustomerResponse `json:"customer,omitempty"` - Message string `json:"message,omitempty"` -} - -type CustomerResponse struct { - ID int64 `json:"id"` - Name string `json:"name"` - Email string `json:"email"` - Phone string `json:"phone"` - BirthDate string `json:"birth_date,omitempty"` - Points int `json:"points"` - CreatedAt string `json:"created_at"` - CustomerID string `json:"customer_id"` -} - -func MapToMemberRegistrationResponse(entity *entity.MemberRegistrationResponse) MemberRegistrationResponse { - return MemberRegistrationResponse{ - Token: entity.Token, - Status: entity.Status, - ExpiresAt: entity.ExpiresAt, - Message: entity.Message, - } -} - -func MapToMemberVerificationResponse(user *entity.AuthenticateUser) LoginResponseCustoemr { - resp := LoginResponseCustoemr{ - ID: user.ID, - Token: user.Token, - Name: user.Name, - ResetPassword: user.ResetPassword, - } - - return resp -} - -func MapToMemberRegistrationStatus(entity *entity.MemberRegistrationStatus) MemberRegistrationStatus { - return MemberRegistrationStatus{ - Token: entity.Token, - Status: entity.Status, - ExpiresAt: entity.ExpiresAt, - IsExpired: entity.IsExpired, - CreatedAt: entity.CreatedAt, - } -} - -func MapToResendOTPResponse(entity *entity.ResendOTPResponse) ResendOTPResponse { - return ResendOTPResponse{ - Token: entity.Token, - ExpiresAt: entity.ExpiresAt, - Message: entity.Message, - } -} - -func MapToCustomerCheckResponse(entity *entity.CustomerCheckResponse) CustomerCheckResponse { - response := CustomerCheckResponse{ - Exists: entity.Exists, - Message: entity.Message, - } - - if entity.Customer != nil { - customer := &CustomerResponse{ - ID: entity.Customer.ID, - Name: entity.Customer.Name, - Email: entity.Customer.Email, - Phone: entity.Customer.Phone, - CreatedAt: entity.Customer.CreatedAt.Format("2006-01-02"), - } - response.Customer = customer - } - - return response -} diff --git a/internal/handlers/response/order.go b/internal/handlers/response/order.go deleted file mode 100644 index 83cd5ad..0000000 --- a/internal/handlers/response/order.go +++ /dev/null @@ -1,256 +0,0 @@ -package response - -import ( - "enaklo-pos-be/internal/constants/order" - "enaklo-pos-be/internal/constants/transaction" - "enaklo-pos-be/internal/entity" - "time" -) - -type Order struct { - ID int64 `json:"id" ` - BranchID int64 `json:"branch_id" ` - BranchName string `json:"branch_name" ` - Amount float64 `json:"amount" ` - Status order.OrderStatus `json:"status" ` - CustomerName string `json:"customer_name" ` - CustomerPhone string `json:"customer_phone" ` - Pax int `json:"pax" ` - PaymentMethod transaction.PaymentMethod `json:"payment_method" ` - OrderItem []OrderItem `json:"order_items" ` - CreatedAt string `json:"created_at"` - UpdatedAt string `json:"updated_at"` -} - -type OrderAmount struct { - Amount float64 `json:"amount"` -} - -type HistoryOrder struct { - ID int64 `json:"id"` - Employee string `json:"employee"` - Site string `json:"site"` - Timestamp string `json:"timestamp"` - BookingTime string `json:"booking_time"` - Tickets []string `json:"tickets"` - PaymentType string `json:"payment_type"` - Status string `json:"status"` - Amount float64 `json:"amount"` - VisitDate string `json:"visit_date"` - TicketStatus string `json:"ticket_status"` - Source string `json:"source"` -} - -type OrderItem struct { - OrderItemID int64 `json:"order_item_id" ` - ItemID int64 `json:"item_id" ` - ItemType order.ItemType `json:"item_type" ` - ItemName string `json:"item_name" ` - Price float64 `json:"price" ` - Qty int64 `json:"qty" ` - CreatedAt string `json:"created_at"` - UpdatedAt string `json:"updated_at"` -} - -type OrderList struct { - Orders []Order `json:"orders"` - Total int64 `json:"total"` - Limit int `json:"limit"` - Offset int `json:"offset"` -} - -type HistoryOrderList struct { - Orders []HistoryOrder `json:"history_orders"` - Total int64 `json:"total"` - Limit int `json:"limit"` - Offset int `json:"offset"` -} - -type TicketSold struct { - Count int64 `json:"count"` -} - -type OrderMonthlyRevenue struct { - TotalRevenue float64 `json:"total_revenue"` - TotalTransaction int64 `json:"total_transaction"` -} - -type OrderBranchRevenue struct { - BranchID string `json:"branch_id"` - BranchName string `json:"name"` - BranchLocation string `json:"location"` - TotalTransaction int `json:"total_trans"` - TotalAmount float64 `json:"total_amount"` -} - -type CreateOrderResponse struct { - ID int64 `json:"id"` - PartnerID int64 `json:"partner_id"` - Status string `json:"status"` - Amount float64 `json:"amount"` - Total float64 `json:"total"` - Tax float64 `json:"tax"` - PaymentType string `json:"payment_type"` - CreatedAt time.Time `json:"created_at"` - OrderItems []CreateOrderItemResponse `json:"order_items"` -} - -type PrintDetailResponse struct { - ID int64 `json:"id"` - Logo string `json:"logo"` - OrderID string `json:"order_id"` - PartnerName string `json:"partner_name"` - Total float64 `json:"total"` - Fee float64 `json:"fee"` - PaymentType string `json:"payment_type"` - Source string `json:"source"` - VisitDateAt string `json:"visit_date_at"` - VisitTime string `json:"visit_time"` - OrderItems []CreateOrderItemResponse `json:"order_items"` - CasheerName string `json:"casheer_name"` -} - -type ExecuteOrderResponse struct { - ID int64 `json:"id"` - PartnerID int64 `json:"partner_id"` - Status string `json:"status"` - Amount float64 `json:"amount"` - PaymentType string `json:"payment_type"` - CreatedAt time.Time `json:"created_at"` - OrderItems []CreateOrderItemResponse `json:"order_items"` - PaymentToken string `json:"payment_token"` - RedirectURL string `json:"redirect_url"` - QRcode string `json:"qr_code"` - VirtualAccount string `json:"virtual_account"` - BankName string `json:"bank_name"` - BankCode string `json:"bank_code"` -} - -type ExecuteCheckinResponse struct { - ID int64 `json:"id"` - PartnerID int64 `json:"partner_id"` - Status string `json:"status"` - Amount float64 `json:"amount"` - PaymentType string `json:"payment_type"` - CreatedAt time.Time `json:"created_at"` - OrderItems []CreateOrderItemResponse `json:"order_items"` - PaymentToken string `json:"payment_token"` - RedirectURL string `json:"redirect_url"` - QRcode string `json:"qr_code"` -} - -type CheckingInquiryResponse struct { - Token string `json:"token"` -} - -type CreateOrderItemResponse struct { - ID int64 `json:"id"` - ItemID int64 `json:"item_id"` - Quantity int `json:"quantity"` - Price float64 `json:"price"` - Name string `json:"name"` -} - -type ProductDailySales struct { - Day time.Time `json:"day"` - SiteID int64 `json:"site_id"` - SiteName string `json:"site_name"` - PaymentType string `json:"payment_type"` - Total float64 `json:"total"` -} - -type PaymentDistribution struct { - PaymentType string `json:"payment_type"` - Count int `json:"count"` -} - -type OrderDetail struct { - ID int64 `json:"id"` - QRCode string `json:"qr_code"` - SiteName string `json:"site_name"` - FullName string `json:"full_name"` - Email string `json:"email"` - PhoneNumber string `json:"phone_number"` - OrderItems []OrderDetailItem `json:"order_items"` - TotalAmount float64 `json:"total_amount"` - CreatedAt time.Time `json:"created_at"` - Status string `json:"status"` - PaymentLink string `json:"payment_link"` - PaymentToken string `json:"payment_token"` - Fee float64 `json:"fee"` -} - -type OrderDetailItem struct { - Name string `json:"name"` - ItemType string `json:"item_type"` - Description string `json:"description"` - Quantity int `json:"quantity"` // Quantity of the item - UnitPrice float64 `json:"unit_price"` // Price per unit - TotalPrice float64 `json:"total_price"` // Total price for this item (Quantity * UnitPrice) -} - -type OrderHistoryResponse struct { - ID int64 `json:"id"` - IsMember bool `json:"is_member"` - CustomerID *int64 `json:"customer_id"` - CustomerName string `json:"customer_name"` - Status string `json:"status"` - Amount float64 `json:"amount"` - Total float64 `json:"total"` - PaymentType string `json:"payment_type"` - TableNumber string `json:"table_number"` - OrderType string `json:"order_type"` - OrderItems []OrderItemResponse `json:"order_items"` - CreatedAt string `json:"created_at"` - Tax float64 `json:"tax"` - RestaurantName string `json:"restaurant_name"` -} - -func MapOrderHistoryResponse(orders []*entity.Order) []OrderHistoryResponse { - responseData := make([]OrderHistoryResponse, 0, len(orders)) - - for _, order := range orders { - orderResponse := mapOrderToResponse(order) - responseData = append(responseData, orderResponse) - } - - return responseData -} - -func mapOrderToResponse(order *entity.Order) OrderHistoryResponse { - paymentFormatter := NewPaymentFormatter() - return OrderHistoryResponse{ - ID: order.ID, - CustomerName: order.CustomerName, - CustomerID: order.CustomerID, - IsMember: order.IsMemberOrder(), - Status: order.Status, - Amount: order.Amount, - Total: order.Total, - PaymentType: paymentFormatter.Format(order.PaymentType, order.PaymentProvider), - TableNumber: order.TableNumber, - OrderType: order.OrderType, - OrderItems: mapOrderItems(order.OrderItems), - CreatedAt: order.CreatedAt.Format(time.RFC3339), - Tax: order.Tax, - } -} - -func mapOrderItems(items []entity.OrderItem) []OrderItemResponse { - orderItems := make([]OrderItemResponse, 0, len(items)) - - for _, item := range items { - orderItems = append(orderItems, OrderItemResponse{ - OrderItemID: item.ID, - ProductID: item.ItemID, - ProductName: item.ItemName, - Price: item.Price, - Quantity: item.Quantity, - Subtotal: item.Price * float64(item.Quantity), - Notes: item.Notes, - Status: item.Status, - }) - } - - return orderItems -} diff --git a/internal/handlers/response/order_inquiry.go b/internal/handlers/response/order_inquiry.go deleted file mode 100644 index 2671dad..0000000 --- a/internal/handlers/response/order_inquiry.go +++ /dev/null @@ -1,133 +0,0 @@ -package response - -import ( - "enaklo-pos-be/internal/entity" - "time" -) - -type OrderInquiryResponse struct { - ID string `json:"id"` - Status string `json:"status"` - Amount float64 `json:"amount"` - Tax float64 `json:"tax"` - Total float64 `json:"total"` - CustomerID int64 `json:"customer_id"` - PaymentType string `json:"payment_type"` - CustomerName string `json:"customer_name"` - CustomerPhoneNumber string `json:"customer_phone_number"` - CustomerEmail string `json:"customer_email"` - ExpiresAt time.Time `json:"expires_at"` - CreatedAt time.Time `json:"created_at"` - Items []OrderItemResponse `json:"items"` - Token string `json:"token"` -} - -type OrderItemResponse struct { - OrderItemID int64 `json:"order_item_id"` - ProductID int64 `json:"product_id"` - ProductName string `json:"product_name"` - Price float64 `json:"price"` - Quantity int `json:"quantity"` - Subtotal float64 `json:"subtotal"` - Notes string `json:"notes"` - Status string `json:"status"` -} - -func mapToOrderItemResponses(items []entity.OrderItem) []OrderItemResponse { - result := make([]OrderItemResponse, 0, len(items)) - for _, item := range items { - productName := "" - if item.Product != nil { - productName = item.Product.Name - } - - result = append(result, OrderItemResponse{ - ProductID: item.ItemID, - ProductName: productName, - Price: item.Price, - Quantity: item.Quantity, - Subtotal: item.Price * float64(item.Quantity), - }) - } - return result -} - -func MapToInquiryResponse(result *entity.OrderInquiryResponse) OrderInquiryResponse { - resp := OrderInquiryResponse{ - ID: result.OrderInquiry.ID, - Status: result.OrderInquiry.Status, - Amount: result.OrderInquiry.Amount, - Tax: result.OrderInquiry.Tax, - Total: result.OrderInquiry.Total, - CustomerID: result.OrderInquiry.CustomerID, - PaymentType: result.OrderInquiry.PaymentType, - ExpiresAt: result.OrderInquiry.ExpiresAt, - CreatedAt: result.OrderInquiry.CreatedAt, - Items: mapToOrderItemResponses(result.OrderInquiry.OrderItems), - Token: result.Token, - CustomerName: result.OrderInquiry.CustomerName, - CustomerEmail: result.OrderInquiry.CustomerEmail, - CustomerPhoneNumber: result.OrderInquiry.CustomerPhoneNumber, - } - - return resp -} - -type OrderResponse struct { - ID int64 `json:"id"` - Status string `json:"status"` - Amount float64 `json:"amount"` - Tax float64 `json:"tax"` - Total float64 `json:"total"` - CustomerName string `json:"customer_name,omitempty"` - PaymentType string `json:"payment_type"` - Source string `json:"source"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt *time.Time `json:"updated_at,omitempty"` - Items []OrderItemResponse `json:"items"` - TableNumber string `json:"table_number"` - OrderType string `json:"order_type"` -} - -func MapToOrderResponse(result *entity.OrderResponse) OrderResponse { - resp := OrderResponse{ - ID: result.Order.ID, - Status: result.Order.Status, - Amount: result.Order.Amount, - Tax: result.Order.Tax, - Total: result.Order.Total, - PaymentType: formatPayment(result.Order.PaymentType, result.Order.PaymentProvider), - CreatedAt: result.Order.CreatedAt, - Items: MapToOrderItemResponses(result.Order.OrderItems), - CustomerName: result.Order.CustomerName, - TableNumber: result.Order.TableNumber, - OrderType: result.Order.OrderType, - } - - return resp -} - -func MapToOrderItemResponses(items []entity.OrderItem) []OrderItemResponse { - result := make([]OrderItemResponse, 0, len(items)) - for _, item := range items { - result = append(result, OrderItemResponse{ - OrderItemID: item.ID, - ProductID: item.ItemID, - ProductName: item.ItemName, - Price: item.Price, - Quantity: item.Quantity, - Subtotal: item.Price * float64(item.Quantity), - Notes: item.Notes, - Status: item.Status, - }) - } - return result -} - -func formatPayment(payment, provider string) string { - if payment == "CASH" { - return payment - } - - return payment + " " + provider -} diff --git a/internal/handlers/response/pagination_formatter.go b/internal/handlers/response/pagination_formatter.go deleted file mode 100644 index a8be115..0000000 --- a/internal/handlers/response/pagination_formatter.go +++ /dev/null @@ -1,34 +0,0 @@ -package response - -type PaginationHelper struct{} - -func NewPaginationHelper() *PaginationHelper { - return &PaginationHelper{} -} - -func (p *PaginationHelper) BuildPagingMeta(offset, limit int, total int64) *PagingMeta { - page := 1 - if limit > 0 { - page = (offset / limit) + 1 - } - - return &PagingMeta{ - Page: page, - Total: total, - Limit: limit, - } -} - -func (p *PaginationHelper) calculateTotalPages(total int64, limit int) int { - if limit <= 0 { - return 1 - } - return int((total + int64(limit) - 1) / int64(limit)) -} - -func (p *PaginationHelper) hasNextPage(offset, limit int, total int64) bool { - if limit <= 0 { - return false - } - return int64(offset+limit) < total -} diff --git a/internal/handlers/response/paging.gp.go b/internal/handlers/response/paging.gp.go deleted file mode 100644 index dc5d0e5..0000000 --- a/internal/handlers/response/paging.gp.go +++ /dev/null @@ -1,7 +0,0 @@ -package response - -type PagingMeta struct { - Page int `json:"page"` - Limit int `json:"limit"` - Total int64 `json:"total"` -} diff --git a/internal/handlers/response/partner.go b/internal/handlers/response/partner.go deleted file mode 100644 index 7933bd8..0000000 --- a/internal/handlers/response/partner.go +++ /dev/null @@ -1,25 +0,0 @@ -package response - -type Partner struct { - ID *int64 `json:"id"` - Name string `json:"name"` - Status string `json:"status"` - Address string `json:"address"` - CreatedAt string `json:"created_at,omitempty"` - UpdatedAt string `json:"updated_at,omitempty"` - AdminName string `json:"admin_name"` - AdminPhoneNumber string `json:"admin_phone_number"` - AdminEmail string `json:"admin_email"` - Balance float64 `json:"balance"` - BankAccountName string `json:"bank_account_name"` - BankAccountHolderName string `json:"bank_account_holder_name"` - BankAccountHolderNumber string `json:"bank_account_holder_number"` - Logo string `json:"logo"` -} - -type PartnerList struct { - Partners []Partner `json:"partners"` - Total int64 `json:"total"` - Limit int `json:"limit"` - Offset int `json:"offset"` -} diff --git a/internal/handlers/response/payment_formatter.go b/internal/handlers/response/payment_formatter.go deleted file mode 100644 index 5a3f5f5..0000000 --- a/internal/handlers/response/payment_formatter.go +++ /dev/null @@ -1,20 +0,0 @@ -package response - -import "fmt" - -type PaymentFormatter interface { - Format(paymentType, paymentProvider string) string -} - -type paymentFormatter struct{} - -func NewPaymentFormatter() PaymentFormatter { - return &paymentFormatter{} -} - -func (f *paymentFormatter) Format(paymentType, paymentProvider string) string { - if paymentProvider != "" { - return fmt.Sprintf("%s (%s)", paymentType, paymentProvider) - } - return paymentType -} diff --git a/internal/handlers/response/product.go b/internal/handlers/response/product.go deleted file mode 100644 index e726679..0000000 --- a/internal/handlers/response/product.go +++ /dev/null @@ -1,24 +0,0 @@ -package response - -type Product struct { - ID int64 `json:"id"` - Name string `json:"name"` - Type string `json:"type"` - Price float64 `json:"price"` - Status string `json:"status"` - Description string `json:"description"` - Image string `json:"image"` - Category Category `json:"category"` -} - -type Category struct { - ID int64 `json:"id"` - Name string `json:"name"` -} - -type ProductList struct { - Products []Product `json:"products"` - Total int64 `json:"total"` - Limit int `json:"limit"` - Offset int `json:"offset"` -} diff --git a/internal/handlers/response/site.go b/internal/handlers/response/site.go deleted file mode 100644 index ee365d5..0000000 --- a/internal/handlers/response/site.go +++ /dev/null @@ -1,41 +0,0 @@ -package response - -type Site struct { - ID *int64 `json:"id"` - Name string `json:"name"` - PartnerID int64 `json:"partner_id"` - Image string `json:"image"` - Address string `json:"address"` - LocationLink string `json:"location_link"` - Description string `json:"description"` - Highlight string `json:"highlight"` - ContactPerson string `json:"contact_person"` - TnC string `json:"tnc"` - AdditionalInfo string `json:"additional_info"` - Status string `json:"status"` - IsSeasonTicket bool `json:"is_season_ticket"` - IsDiscountActive bool `json:"is_discount_active"` - CreatedAt string `json:"created_at"` - UpdatedAt string `json:"updated_at"` - Products []Product `json:"products"` - Lat float64 `json:"lat"` - Long float64 `json:"long"` - Region string `json:"region"` - Regency string `json:"regency"` -} - -type SiteName struct { - ID *int64 `json:"id"` - Name string `json:"name"` -} - -type SiteCount struct { - Count int `json:"count"` -} - -type SiteList struct { - Sites []Site `json:"sites"` - Total int64 `json:"total"` - Limit int `json:"limit"` - Offset int `json:"offset"` -} diff --git a/internal/handlers/response/studio.go b/internal/handlers/response/studio.go deleted file mode 100644 index d552930..0000000 --- a/internal/handlers/response/studio.go +++ /dev/null @@ -1,19 +0,0 @@ -package response - -type Studio struct { - ID *int64 `json:"id"` - BranchId *int64 `json:"branch_id"` - Name string `json:"name"` - Status string `json:"status"` - Price float64 `json:"price"` - CreatedAt string `json:"created_at"` - Metadata map[string]interface{} `json:"metadata"` - UpdatedAt string `json:"updated_at"` -} - -type StudioList struct { - Studios []Studio `json:"studios"` - Total int64 `json:"total"` - Limit int `json:"limit"` - Offset int `json:"offset"` -} diff --git a/internal/handlers/response/transaction.go b/internal/handlers/response/transaction.go deleted file mode 100644 index d66baea..0000000 --- a/internal/handlers/response/transaction.go +++ /dev/null @@ -1,20 +0,0 @@ -package response - -type TransactionListItem struct { - ID string `json:"id"` - TransactionType string `json:"transaction_type"` - Status string `json:"status"` - CreatedAt string `json:"created_at"` - SiteName string `json:"site_name"` - Amount int64 `json:"amount"` - PartnerName string `json:"partner_name"` - Total int64 `json:"total"` - Fee int64 `json:"fee"` -} - -type TransactionListResponse struct { - Transactions []TransactionListItem `json:"transactions"` - TotalCount int `json:"total_count"` - Limit int `json:"limit"` - Offset int `json:"offset"` -} diff --git a/internal/handlers/response/undian.go b/internal/handlers/response/undian.go deleted file mode 100644 index 1bef236..0000000 --- a/internal/handlers/response/undian.go +++ /dev/null @@ -1,79 +0,0 @@ -package response - -// UndianListResponse represents the response for undian list API -type UndianListResponse struct { - Events []UndianEventResponse `json:"events"` -} - -// UndianEventResponse represents an undian event with customer's vouchers and prizes -type UndianEventResponse struct { - ID int64 `json:"id"` - Title string `json:"title"` - Description *string `json:"description"` - ImageURL *string `json:"image_url"` - Status string `json:"status"` - StartDate string `json:"start_date"` - EndDate string `json:"end_date"` - DrawDate string `json:"draw_date"` - MinimumPurchase float64 `json:"minimum_purchase"` - DrawCompleted bool `json:"draw_completed"` - DrawCompletedAt *string `json:"draw_completed_at"` - TermsConditions *string `json:"terms_and_conditions"` - Prefix *string `json:"prefix"` - CreatedAt string `json:"created_at"` - UpdatedAt string `json:"updated_at"` - VoucherCount int `json:"voucher_count"` - TotalPrizes int `json:"total_prizes"` - Vouchers []UndianVoucherResponse `json:"vouchers"` - Prizes []UndianPrizeResponse `json:"prizes"` -} - -// UndianVoucherResponse represents a customer's voucher -type UndianVoucherResponse struct { - ID int64 `json:"id"` - VoucherCode string `json:"voucher_code"` - VoucherNumber *int `json:"voucher_number"` - IsWinner bool `json:"is_winner"` - PrizeRank *int `json:"prize_rank"` - WonAt *string `json:"won_at"` - CreatedAt string `json:"created_at"` -} - -// UndianPrizeResponse represents a prize in the undian event -type UndianPrizeResponse struct { - ID int64 `json:"id"` - Rank int `json:"rank"` - PrizeName string `json:"prize_name"` - PrizeValue *float64 `json:"prize_value"` - PrizeDescription *string `json:"prize_description"` - PrizeType string `json:"prize_type"` - PrizeImageURL *string `json:"prize_image_url"` - WinningVoucherID *int64 `json:"winning_voucher_id,omitempty"` - WinnerUserID *int64 `json:"winner_user_id,omitempty"` - IsWon bool `json:"is_won"` - Amount *int64 `json:"amount"` -} - -// ActiveEventsResponse represents the response for active events API (without customer data) -type ActiveEventsResponse struct { - Events []ActiveEventResponse `json:"events"` -} - -// ActiveEventResponse represents an active undian event (without customer-specific data) -type ActiveEventResponse struct { - ID int64 `json:"id"` - Title string `json:"title"` - Description *string `json:"description"` - ImageURL *string `json:"image_url"` - Status string `json:"status"` - StartDate string `json:"start_date"` - EndDate string `json:"end_date"` - DrawDate string `json:"draw_date"` - MinimumPurchase float64 `json:"minimum_purchase"` - DrawCompleted bool `json:"draw_completed"` - DrawCompletedAt *string `json:"draw_completed_at"` - TermsConditions *string `json:"terms_and_conditions"` - Prefix *string `json:"prefix"` - CreatedAt string `json:"created_at"` - UpdatedAt string `json:"updated_at"` -} diff --git a/internal/handlers/response/user.go b/internal/handlers/response/user.go deleted file mode 100644 index 585c7b4..0000000 --- a/internal/handlers/response/user.go +++ /dev/null @@ -1,55 +0,0 @@ -package response - -import "time" - -type User struct { - ID int64 `json:"id"` - Name string `json:"name"` - Email string `json:"email"` - PhoneNumber string `json:"phone_number"` - NIK string `json:"nik"` - Status string `json:"status"` - RoleID int64 `json:"role_id"` - RoleName string `json:"role_name"` - PartnerID *int64 `json:"partner_id"` - SiteID *int64 `json:"site_id"` - PartnerName string `json:"partner_name"` - CreatedAt string `json:"created_at,omitempty"` - UpdatedAt string `json:"updated_at,omitempty"` -} - -type UserList struct { - Users []User `json:"users"` - Total int64 `json:"total"` - Limit int `json:"limit"` - Offset int `json:"offset"` -} -type Customer struct { - ID int64 `json:"id"` - Name string `json:"name"` - Email string `json:"email"` - PhoneNumber string `json:"phone_number"` - Status string `json:"status"` - RoleID int64 `json:"role_id"` - RoleName string `json:"role_name"` - PartnerID *int64 `json:"partner_id"` - PartnerName string `json:"partner_name"` - CreatedAt string `json:"created_at,omitempty"` - UpdatedAt string `json:"updated_at,omitempty"` -} - -type CustomerList struct { - Users []Customer `json:"users"` - Total int64 `json:"total"` - Limit int `json:"limit"` - Offset int `json:"offset"` -} - -type UserRegister struct { - ID int64 `json:"id"` - Name string `json:"name"` - Email string `json:"email"` - PhoneNumber string `json:"phone_number"` - CreatedAt time.Time `json:"created_at,omitempty"` - UpdatedAt time.Time `json:"updated_at,omitempty"` -} diff --git a/internal/logger/app_logger.go b/internal/logger/app_logger.go new file mode 100644 index 0000000..96885b4 --- /dev/null +++ b/internal/logger/app_logger.go @@ -0,0 +1,134 @@ +package logger + +import ( + "apskel-pos-be/internal/appcontext" + "context" + "log" + "os" + + "github.com/sirupsen/logrus" +) + +type logCtxKeyType struct{} + +var logCtxKey = logCtxKeyType(struct{}{}) + +var logger *logrus.Logger + +const ( + LogMethod = "Method" + LogError = "Error" +) + +func Setup(logLevel, logFormat string) { + level, err := logrus.ParseLevel(logLevel) + if err != nil { + log.Fatal(err.Error()) + } + + logger = &logrus.Logger{ + Out: os.Stdout, + Hooks: make(logrus.LevelHooks), + Level: level, + Formatter: &logrus.JSONFormatter{}, + } + + if logFormat != "json" { + logger.Formatter = &logrus.TextFormatter{} + } + NonContext = &ContextLogger{ + entry: logrus.NewEntry(logger), + } +} + +type ContextLogger struct { + entry *logrus.Entry +} + +var NonContext *ContextLogger + +func NewContextLogger(ctx interface{}, method string) *ContextLogger { + logEntry := logger.WithFields(appcontext.LogFields(ctx)).WithField(LogMethod, method) + return &ContextLogger{ + entry: logEntry, + } +} + +func (l *ContextLogger) Fatal(errMessage string, err error) { + l.entry. + WithField(LogError, err). + Fatal(errMessage) +} + +func (l *ContextLogger) Error(errMessage string, err error) { + l.entry.WithField(LogError, err).Error(errMessage) +} + +func (l *ContextLogger) Errorf(err error, errMessageFormat string, errMessages ...interface{}) { + l.entry.WithField(LogError, err).Errorf(errMessageFormat, errMessages...) +} + +func (l *ContextLogger) ErrorWithFields(msg string, fields map[string]interface{}, err error) { + for key, val := range fields { + l.entry = l.entry.WithField(key, val) + } + + l.entry.WithField(LogError, err).Error(msg) +} + +func (l *ContextLogger) Info(msg string) { + l.entry.Info(msg) + +} + +func (l *ContextLogger) Infof(msg string, args ...interface{}) { + l.entry.Infof(msg, args...) +} + +func (l *ContextLogger) Debugf(msg string, args ...interface{}) { + l.entry.Debugf(msg, args...) +} + +func (l *ContextLogger) Debug(msg string) { + l.entry.Debug(msg) +} + +func (l *ContextLogger) InfoWithFields(msg string, fields map[string]interface{}) { + for key, val := range fields { + l.entry = l.entry.WithField(key, val) + } + + l.entry.Info(msg) +} + +func (l *ContextLogger) DebugWithFields(msg string, fields map[string]interface{}) { + for key, val := range fields { + l.entry = l.entry.WithField(key, val) + } + + l.entry.Debug(msg) +} + +func (l *ContextLogger) Warn(msg string) { + l.entry.Warn(msg) +} + +func (l *ContextLogger) Warnf(msg string, args ...interface{}) { + l.entry.Warnf(msg, args...) +} + +func (l *ContextLogger) WarnWithFields(msg string, fields map[string]interface{}, err error) { + for key, val := range fields { + l.entry = l.entry.WithField(key, val) + } + + l.entry.WithField(LogError, err) + l.entry.Warn(msg) +} + +func FromContext(ctx context.Context) *logrus.Entry { + if entry, ok := ctx.Value(logCtxKey).(*logrus.Entry); ok { + return entry + } + return logger.WithFields(map[string]interface{}{}) +} diff --git a/internal/mappers/category_mapper.go b/internal/mappers/category_mapper.go new file mode 100644 index 0000000..931e51b --- /dev/null +++ b/internal/mappers/category_mapper.go @@ -0,0 +1,154 @@ +package mappers + +import ( + "apskel-pos-be/internal/entities" + "apskel-pos-be/internal/models" +) + +func CategoryEntityToModel(entity *entities.Category) *models.Category { + if entity == nil { + return nil + } + + return &models.Category{ + ID: entity.ID, + OrganizationID: entity.OrganizationID, + Name: entity.Name, + Description: entity.Description, + ImageURL: nil, // Entity doesn't have ImageURL, model does + SortOrder: 0, // Entity doesn't have SortOrder, model does + IsActive: true, // Entity doesn't have IsActive, default to true + CreatedAt: entity.CreatedAt, + UpdatedAt: entity.UpdatedAt, + } +} + +func CategoryModelToEntity(model *models.Category) *entities.Category { + if model == nil { + return nil + } + + metadata := entities.Metadata{} + if model.ImageURL != nil { + metadata["image_url"] = *model.ImageURL + } + metadata["sort_order"] = model.SortOrder + + return &entities.Category{ + ID: model.ID, + OrganizationID: model.OrganizationID, + Name: model.Name, + Description: model.Description, + BusinessType: "restaurant", // Default business type + Metadata: metadata, + CreatedAt: model.CreatedAt, + UpdatedAt: model.UpdatedAt, + } +} + +func CreateCategoryRequestToEntity(req *models.CreateCategoryRequest) *entities.Category { + if req == nil { + return nil + } + + metadata := entities.Metadata{} + if req.ImageURL != nil { + metadata["image_url"] = *req.ImageURL + } + metadata["sort_order"] = req.SortOrder + + return &entities.Category{ + OrganizationID: req.OrganizationID, + Name: req.Name, + Description: req.Description, + BusinessType: "restaurant", // Default business type + Metadata: metadata, + } +} + +func CategoryEntityToResponse(entity *entities.Category) *models.CategoryResponse { + if entity == nil { + return nil + } + + // Extract image URL and sort order from metadata + var imageURL *string + var sortOrder int + + if entity.Metadata != nil { + if imgURL, exists := entity.Metadata["image_url"]; exists { + if imgURLStr, ok := imgURL.(string); ok { + imageURL = &imgURLStr + } + } + if sort, exists := entity.Metadata["sort_order"]; exists { + if sortInt, ok := sort.(int); ok { + sortOrder = sortInt + } else if sortFloat, ok := sort.(float64); ok { + sortOrder = int(sortFloat) + } + } + } + + return &models.CategoryResponse{ + ID: entity.ID, + OrganizationID: entity.OrganizationID, + Name: entity.Name, + Description: entity.Description, + ImageURL: imageURL, + SortOrder: sortOrder, + IsActive: true, // Default to true since entity doesn't have this field + CreatedAt: entity.CreatedAt, + UpdatedAt: entity.UpdatedAt, + } +} + +func UpdateCategoryEntityFromRequest(entity *entities.Category, req *models.UpdateCategoryRequest) { + if entity == nil || req == nil { + return + } + + if req.Name != nil { + entity.Name = *req.Name + } + + if req.Description != nil { + entity.Description = req.Description + } + + if entity.Metadata == nil { + entity.Metadata = make(entities.Metadata) + } + + if req.ImageURL != nil { + entity.Metadata["image_url"] = *req.ImageURL + } + + if req.SortOrder != nil { + entity.Metadata["sort_order"] = *req.SortOrder + } +} + +func CategoryEntitiesToModels(entities []*entities.Category) []*models.Category { + if entities == nil { + return nil + } + + models := make([]*models.Category, len(entities)) + for i, entity := range entities { + models[i] = CategoryEntityToModel(entity) + } + return models +} + +func CategoryEntitiesToResponses(entities []*entities.Category) []*models.CategoryResponse { + if entities == nil { + return nil + } + + responses := make([]*models.CategoryResponse, len(entities)) + for i, entity := range entities { + responses[i] = CategoryEntityToResponse(entity) + } + return responses +} diff --git a/internal/mappers/customer_mapper.go b/internal/mappers/customer_mapper.go new file mode 100644 index 0000000..d373650 --- /dev/null +++ b/internal/mappers/customer_mapper.go @@ -0,0 +1,71 @@ +package mappers + +import ( + "apskel-pos-be/internal/entities" + "apskel-pos-be/internal/models" + + "github.com/google/uuid" +) + +// ToCustomerResponse converts a customer entity to a customer response +func ToCustomerResponse(customer *entities.Customer) *models.CustomerResponse { + if customer == nil { + return nil + } + + return &models.CustomerResponse{ + ID: customer.ID, + OrganizationID: customer.OrganizationID, + Name: customer.Name, + Email: customer.Email, + Phone: customer.Phone, + Address: customer.Address, + IsDefault: customer.IsDefault, + IsActive: customer.IsActive, + Metadata: customer.Metadata, + CreatedAt: customer.CreatedAt, + UpdatedAt: customer.UpdatedAt, + } +} + +// ToCustomerResponses converts a slice of customer entities to customer responses +func ToCustomerResponses(customers []entities.Customer) []models.CustomerResponse { + responses := make([]models.CustomerResponse, len(customers)) + for i, customer := range customers { + responses[i] = *ToCustomerResponse(&customer) + } + return responses +} + +// ToCustomerEntity converts a create customer request to a customer entity +func ToCustomerEntity(req *models.CreateCustomerRequest, organizationID uuid.UUID) *entities.Customer { + return &entities.Customer{ + OrganizationID: organizationID, + Name: req.Name, + Email: req.Email, + Phone: req.Phone, + Address: req.Address, + IsDefault: false, // New customers are not default by default + IsActive: true, + Metadata: entities.Metadata{}, + } +} + +// UpdateCustomerEntity updates a customer entity with update request data +func UpdateCustomerEntity(customer *entities.Customer, req *models.UpdateCustomerRequest) { + if req.Name != nil { + customer.Name = *req.Name + } + if req.Email != nil { + customer.Email = req.Email + } + if req.Phone != nil { + customer.Phone = req.Phone + } + if req.Address != nil { + customer.Address = req.Address + } + if req.IsActive != nil { + customer.IsActive = *req.IsActive + } +} diff --git a/internal/mappers/file_mapper.go b/internal/mappers/file_mapper.go new file mode 100644 index 0000000..98ebc2b --- /dev/null +++ b/internal/mappers/file_mapper.go @@ -0,0 +1,98 @@ +package mappers + +import ( + "apskel-pos-be/internal/constants" + "apskel-pos-be/internal/entities" + "apskel-pos-be/internal/models" + "strings" + + "fmt" + "github.com/google/uuid" +) + +func FileEntityToResponse(file *entities.File) *models.FileResponse { + if file == nil { + return nil + } + + return &models.FileResponse{ + ID: file.ID, + OrganizationID: file.OrganizationID, + UserID: file.UserID, + FileName: file.FileName, + OriginalName: file.OriginalName, + FileURL: file.FileURL, + FileSize: file.FileSize, + MimeType: file.MimeType, + FileType: file.FileType, + UploadPath: file.UploadPath, + IsPublic: file.IsPublic, + Metadata: map[string]interface{}(file.Metadata), + CreatedAt: file.CreatedAt, + UpdatedAt: file.UpdatedAt, + } +} + +func UploadFileRequestToEntity(req *models.UploadFileRequest, organizationID, userID uuid.UUID, fileName, originalName, fileURL, mimeType, uploadPath string, fileSize int64) *entities.File { + if req == nil { + return nil + } + + isPublic := true + if req.IsPublic != nil { + isPublic = *req.IsPublic + } + + return &entities.File{ + OrganizationID: organizationID, + UserID: userID, + FileName: fileName, + OriginalName: originalName, + FileURL: fileURL, + FileSize: fileSize, + MimeType: mimeType, + FileType: string(req.FileType), + UploadPath: uploadPath, + IsPublic: isPublic, + Metadata: entities.Metadata(req.Metadata), + } +} + +// Update request to entity updates +func UpdateFileRequestToEntityUpdates(req *models.UpdateFileRequest) map[string]interface{} { + updates := make(map[string]interface{}) + + if req.IsPublic != nil { + updates["is_public"] = *req.IsPublic + } + if req.Metadata != nil { + updates["metadata"] = entities.Metadata(req.Metadata) + } + + return updates +} + +func FileEntitiesToResponses(files []*entities.File) []*models.FileResponse { + if files == nil { + return nil + } + + responses := make([]*models.FileResponse, len(files)) + for i, file := range files { + response := FileEntityToResponse(file) + if response != nil { + responses[i] = response + } + } + + return responses +} + +func GenerateFileName(originalName string, organizationID, userID uuid.UUID) string { + cleanedName := strings.ReplaceAll(originalName, " ", "") + return fmt.Sprintf("/public/%s_%s", organizationID.String(), cleanedName) +} + +func GetFileTypeFromMimeType(mimeType string) constants.FileType { + return constants.GetFileTypeFromMimeType(mimeType) +} diff --git a/internal/mappers/inventory_mapper.go b/internal/mappers/inventory_mapper.go new file mode 100644 index 0000000..62e980d --- /dev/null +++ b/internal/mappers/inventory_mapper.go @@ -0,0 +1,114 @@ +package mappers + +import ( + "apskel-pos-be/internal/entities" + "apskel-pos-be/internal/models" +) + +func InventoryEntityToModel(entity *entities.Inventory) *models.Inventory { + if entity == nil { + return nil + } + + return &models.Inventory{ + ID: entity.ID, + OutletID: entity.OutletID, + ProductID: entity.ProductID, + Quantity: entity.Quantity, + ReorderLevel: entity.ReorderLevel, + UpdatedAt: entity.UpdatedAt, + } +} + +func InventoryModelToEntity(model *models.Inventory) *entities.Inventory { + if model == nil { + return nil + } + + return &entities.Inventory{ + ID: model.ID, + OutletID: model.OutletID, + ProductID: model.ProductID, + Quantity: model.Quantity, + ReorderLevel: model.ReorderLevel, + UpdatedAt: model.UpdatedAt, + } +} + +func CreateInventoryRequestToEntity(req *models.CreateInventoryRequest) *entities.Inventory { + if req == nil { + return nil + } + + return &entities.Inventory{ + OutletID: req.OutletID, + ProductID: req.ProductID, + Quantity: req.Quantity, + ReorderLevel: req.ReorderLevel, + } +} + +func InventoryEntityToResponse(entity *entities.Inventory) *models.InventoryResponse { + if entity == nil { + return nil + } + + return &models.InventoryResponse{ + ID: entity.ID, + OutletID: entity.OutletID, + ProductID: entity.ProductID, + Quantity: entity.Quantity, + ReorderLevel: entity.ReorderLevel, + IsLowStock: entity.IsLowStock(), + UpdatedAt: entity.UpdatedAt, + } +} + +func UpdateInventoryEntityFromRequest(entity *entities.Inventory, req *models.UpdateInventoryRequest) { + if entity == nil || req == nil { + return + } + + if req.Quantity != nil { + entity.Quantity = *req.Quantity + } + + if req.ReorderLevel != nil { + entity.ReorderLevel = *req.ReorderLevel + } +} + +func InventoryAdjustmentRequestToModel(req *models.InventoryAdjustmentRequest) *models.InventoryAdjustmentRequest { + if req == nil { + return nil + } + + return &models.InventoryAdjustmentRequest{ + Delta: req.Delta, + Reason: req.Reason, + } +} + +func InventoryEntitiesToModels(entities []*entities.Inventory) []*models.Inventory { + if entities == nil { + return nil + } + + models := make([]*models.Inventory, len(entities)) + for i, entity := range entities { + models[i] = InventoryEntityToModel(entity) + } + return models +} + +func InventoryEntitiesToResponses(entities []*entities.Inventory) []*models.InventoryResponse { + if entities == nil { + return nil + } + + responses := make([]*models.InventoryResponse, len(entities)) + for i, entity := range entities { + responses[i] = InventoryEntityToResponse(entity) + } + return responses +} diff --git a/internal/mappers/order_mapper.go b/internal/mappers/order_mapper.go new file mode 100644 index 0000000..7e72b16 --- /dev/null +++ b/internal/mappers/order_mapper.go @@ -0,0 +1,298 @@ +package mappers + +import ( + "apskel-pos-be/internal/constants" + "apskel-pos-be/internal/entities" + "apskel-pos-be/internal/models" + + "github.com/google/uuid" +) + +// Entity to Response mappers +func OrderEntityToResponse(order *entities.Order) *models.OrderResponse { + if order == nil { + return nil + } + + response := &models.OrderResponse{ + ID: order.ID, + OrganizationID: order.OrganizationID, + OutletID: order.OutletID, + UserID: order.UserID, + CustomerID: order.CustomerID, + OrderNumber: order.OrderNumber, + TableNumber: order.TableNumber, + OrderType: constants.OrderType(order.OrderType), + Status: constants.OrderStatus(order.Status), + Subtotal: order.Subtotal, + TaxAmount: order.TaxAmount, + DiscountAmount: order.DiscountAmount, + TotalAmount: order.TotalAmount, + TotalCost: order.TotalCost, + PaymentStatus: constants.PaymentStatus(order.PaymentStatus), + RefundAmount: order.RefundAmount, + IsVoid: order.IsVoid, + IsRefund: order.IsRefund, + VoidReason: order.VoidReason, + VoidedAt: order.VoidedAt, + VoidedBy: order.VoidedBy, + RefundReason: order.RefundReason, + RefundedAt: order.RefundedAt, + RefundedBy: order.RefundedBy, + Metadata: map[string]interface{}(order.Metadata), + CreatedAt: order.CreatedAt, + UpdatedAt: order.UpdatedAt, + } + + // Map order items + if order.OrderItems != nil { + response.OrderItems = make([]models.OrderItemResponse, len(order.OrderItems)) + for i, item := range order.OrderItems { + response.OrderItems[i] = *OrderItemEntityToResponse(&item) + } + } + + // Map payments + if order.Payments != nil { + response.Payments = make([]models.PaymentResponse, len(order.Payments)) + for i, payment := range order.Payments { + response.Payments[i] = *PaymentEntityToResponse(&payment) + } + } + + return response +} + +func OrderItemEntityToResponse(item *entities.OrderItem) *models.OrderItemResponse { + if item == nil { + return nil + } + + response := &models.OrderItemResponse{ + ID: item.ID, + OrderID: item.OrderID, + ProductID: item.ProductID, + ProductVariantID: item.ProductVariantID, + Quantity: item.Quantity, + UnitPrice: item.UnitPrice, + TotalPrice: item.TotalPrice, + UnitCost: item.UnitCost, + TotalCost: item.TotalCost, + RefundAmount: item.RefundAmount, + RefundQuantity: item.RefundQuantity, + IsPartiallyRefunded: item.IsPartiallyRefunded, + IsFullyRefunded: item.IsFullyRefunded, + RefundReason: item.RefundReason, + RefundedAt: item.RefundedAt, + RefundedBy: item.RefundedBy, + Modifiers: []map[string]interface{}(item.Modifiers), + Notes: item.Notes, + Metadata: map[string]interface{}(item.Metadata), + Status: constants.OrderItemStatus(item.Status), + CreatedAt: item.CreatedAt, + UpdatedAt: item.UpdatedAt, + } + + // Set product name if product is preloaded + if item.Product.ID != uuid.Nil { + response.ProductName = item.Product.Name + } + + // Set product variant name if product variant is preloaded + if item.ProductVariant != nil { + response.ProductVariantName = &item.ProductVariant.Name + } + + return response +} + +func PaymentEntityToResponse(payment *entities.Payment) *models.PaymentResponse { + if payment == nil { + return nil + } + + response := &models.PaymentResponse{ + ID: payment.ID, + OrderID: payment.OrderID, + PaymentMethodID: payment.PaymentMethodID, + Amount: payment.Amount, + Status: constants.PaymentTransactionStatus(payment.Status), + TransactionID: payment.TransactionID, + SplitNumber: payment.SplitNumber, + SplitTotal: payment.SplitTotal, + SplitDescription: payment.SplitDescription, + RefundAmount: payment.RefundAmount, + RefundReason: payment.RefundReason, + RefundedAt: payment.RefundedAt, + RefundedBy: payment.RefundedBy, + Metadata: map[string]interface{}(payment.Metadata), + CreatedAt: payment.CreatedAt, + UpdatedAt: payment.UpdatedAt, + } + + // Map payment order items + if payment.PaymentOrderItems != nil { + response.PaymentOrderItems = make([]models.PaymentOrderItemResponse, len(payment.PaymentOrderItems)) + for i, item := range payment.PaymentOrderItems { + response.PaymentOrderItems[i] = *PaymentOrderItemEntityToResponse(&item) + } + } + + return response +} + +func PaymentOrderItemEntityToResponse(item *entities.PaymentOrderItem) *models.PaymentOrderItemResponse { + if item == nil { + return nil + } + + return &models.PaymentOrderItemResponse{ + ID: item.ID, + PaymentID: item.PaymentID, + OrderItemID: item.OrderItemID, + Amount: item.Amount, + CreatedAt: item.CreatedAt, + UpdatedAt: item.UpdatedAt, + } +} + +// Request to Entity mappers +func CreateOrderRequestToEntity(req *models.CreateOrderRequest, organizationID uuid.UUID) *entities.Order { + if req == nil { + return nil + } + + order := &entities.Order{ + OrganizationID: organizationID, + OutletID: req.OutletID, + UserID: req.UserID, + CustomerID: req.CustomerID, + TableNumber: req.TableNumber, + OrderType: entities.OrderType(req.OrderType), + Status: entities.OrderStatusPending, + PaymentStatus: entities.PaymentStatusPending, + IsVoid: false, + IsRefund: false, + Metadata: entities.Metadata(req.Metadata), + } + + return order +} + +func CreateOrderItemRequestToEntity(req *models.CreateOrderItemRequest) *entities.OrderItem { + if req == nil { + return nil + } + + // UnitPrice will be set from database product price, not from request + return &entities.OrderItem{ + ProductID: req.ProductID, + ProductVariantID: req.ProductVariantID, + Quantity: req.Quantity, + UnitPrice: 0, // Will be set from database product price + Modifiers: entities.Modifiers(req.Modifiers), + Notes: req.Notes, + Metadata: entities.Metadata(req.Metadata), + Status: entities.OrderItemStatusPending, + } +} + +func CreatePaymentRequestToEntity(req *models.CreatePaymentRequest) *entities.Payment { + if req == nil { + return nil + } + + payment := &entities.Payment{ + OrderID: req.OrderID, + PaymentMethodID: req.PaymentMethodID, + Amount: req.Amount, + Status: entities.PaymentTransactionStatusPending, + TransactionID: req.TransactionID, + SplitNumber: req.SplitNumber, + SplitTotal: req.SplitTotal, + SplitDescription: req.SplitDescription, + Metadata: entities.Metadata(req.Metadata), + } + + return payment +} + +func CreatePaymentOrderItemRequestToEntity(req *models.CreatePaymentOrderItemRequest) *entities.PaymentOrderItem { + if req == nil { + return nil + } + + return &entities.PaymentOrderItem{ + OrderItemID: req.OrderItemID, + Amount: req.Amount, + } +} + +// Update request to entity updates +func UpdateOrderRequestToEntityUpdates(req *models.UpdateOrderRequest) map[string]interface{} { + updates := make(map[string]interface{}) + + if req.TableNumber != nil { + updates["table_number"] = *req.TableNumber + } + if req.Status != nil { + updates["status"] = entities.OrderStatus(*req.Status) + } + if req.DiscountAmount != nil { + updates["discount_amount"] = *req.DiscountAmount + } + if req.Metadata != nil { + updates["metadata"] = entities.Metadata(req.Metadata) + } + + return updates +} + +// Helper functions for list responses +func OrderEntitiesToResponses(orders []*entities.Order) []models.OrderResponse { + if orders == nil { + return nil + } + + responses := make([]models.OrderResponse, len(orders)) + for i, order := range orders { + response := OrderEntityToResponse(order) + if response != nil { + responses[i] = *response + } + } + + return responses +} + +func OrderItemEntitiesToResponses(items []*entities.OrderItem) []models.OrderItemResponse { + if items == nil { + return nil + } + + responses := make([]models.OrderItemResponse, len(items)) + for i, item := range items { + response := OrderItemEntityToResponse(item) + if response != nil { + responses[i] = *response + } + } + + return responses +} + +func PaymentEntitiesToResponses(payments []*entities.Payment) []models.PaymentResponse { + if payments == nil { + return nil + } + + responses := make([]models.PaymentResponse, len(payments)) + for i, payment := range payments { + response := PaymentEntityToResponse(payment) + if response != nil { + responses[i] = *response + } + } + + return responses +} diff --git a/internal/mappers/order_mapper_test.go b/internal/mappers/order_mapper_test.go new file mode 100644 index 0000000..58131ba --- /dev/null +++ b/internal/mappers/order_mapper_test.go @@ -0,0 +1,142 @@ +package mappers + +import ( + "testing" + "time" + + "apskel-pos-be/internal/entities" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" +) + +func TestOrderItemEntityToResponse_WithProductNames(t *testing.T) { + // Arrange + productID := uuid.New() + variantID := uuid.New() + orderID := uuid.New() + itemID := uuid.New() + + product := entities.Product{ + ID: productID, + Name: "Test Product", + } + + productVariant := entities.ProductVariant{ + ID: variantID, + Name: "Test Variant", + } + + orderItem := &entities.OrderItem{ + ID: itemID, + OrderID: orderID, + ProductID: productID, + ProductVariantID: &variantID, + Quantity: 2, + UnitPrice: 10.0, + TotalPrice: 20.0, + UnitCost: 5.0, + TotalCost: 10.0, + Status: entities.OrderItemStatusPending, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + Product: product, + ProductVariant: &productVariant, + } + + // Act + result := OrderItemEntityToResponse(orderItem) + + // Assert + assert.NotNil(t, result) + assert.Equal(t, itemID, result.ID) + assert.Equal(t, orderID, result.OrderID) + assert.Equal(t, productID, result.ProductID) + assert.Equal(t, "Test Product", result.ProductName) + assert.Equal(t, &variantID, result.ProductVariantID) + assert.Equal(t, "Test Variant", *result.ProductVariantName) + assert.Equal(t, 2, result.Quantity) + assert.Equal(t, 10.0, result.UnitPrice) + assert.Equal(t, 20.0, result.TotalPrice) +} + +func TestOrderItemEntityToResponse_WithoutProductVariant(t *testing.T) { + // Arrange + productID := uuid.New() + orderID := uuid.New() + itemID := uuid.New() + + product := entities.Product{ + ID: productID, + Name: "Test Product", + } + + orderItem := &entities.OrderItem{ + ID: itemID, + OrderID: orderID, + ProductID: productID, + ProductVariantID: nil, + Quantity: 1, + UnitPrice: 15.0, + TotalPrice: 15.0, + UnitCost: 7.0, + TotalCost: 7.0, + Status: entities.OrderItemStatusPending, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + Product: product, + ProductVariant: nil, + } + + // Act + result := OrderItemEntityToResponse(orderItem) + + // Assert + assert.NotNil(t, result) + assert.Equal(t, itemID, result.ID) + assert.Equal(t, orderID, result.OrderID) + assert.Equal(t, productID, result.ProductID) + assert.Equal(t, "Test Product", result.ProductName) + assert.Nil(t, result.ProductVariantID) + assert.Nil(t, result.ProductVariantName) + assert.Equal(t, 1, result.Quantity) + assert.Equal(t, 15.0, result.UnitPrice) + assert.Equal(t, 15.0, result.TotalPrice) +} + +func TestOrderItemEntityToResponse_WithoutProductPreload(t *testing.T) { + // Arrange + productID := uuid.New() + orderID := uuid.New() + itemID := uuid.New() + + // Create order item without preloaded product (empty product) + orderItem := &entities.OrderItem{ + ID: itemID, + OrderID: orderID, + ProductID: productID, + ProductVariantID: nil, + Quantity: 1, + UnitPrice: 10.0, + TotalPrice: 10.0, + UnitCost: 5.0, + TotalCost: 5.0, + Status: entities.OrderItemStatusPending, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + Product: entities.Product{}, // Empty product + ProductVariant: nil, + } + + // Act + result := OrderItemEntityToResponse(orderItem) + + // Assert + assert.NotNil(t, result) + assert.Equal(t, itemID, result.ID) + assert.Equal(t, orderID, result.OrderID) + assert.Equal(t, productID, result.ProductID) + assert.Equal(t, "", result.ProductName) // Should be empty since product is not preloaded + assert.Nil(t, result.ProductVariantID) + assert.Nil(t, result.ProductVariantName) +} diff --git a/internal/mappers/organization_mapper.go b/internal/mappers/organization_mapper.go new file mode 100644 index 0000000..d7a3c98 --- /dev/null +++ b/internal/mappers/organization_mapper.go @@ -0,0 +1,79 @@ +package mappers + +import ( + "apskel-pos-be/internal/constants" + "apskel-pos-be/internal/entities" + "apskel-pos-be/internal/models" +) + +func OrganizationEntityToModel(entity *entities.Organization) *models.Organization { + if entity == nil { + return nil + } + + return &models.Organization{ + ID: entity.ID, + Name: entity.Name, + Email: entity.Email, + PhoneNumber: entity.PhoneNumber, + PlanType: constants.PlanType(entity.PlanType), + CreatedAt: entity.CreatedAt, + UpdatedAt: entity.UpdatedAt, + } +} + +func OrganizationModelToEntity(model *models.Organization) *entities.Organization { + if model == nil { + return nil + } + + return &entities.Organization{ + ID: model.ID, + Name: model.Name, + Email: model.Email, + PhoneNumber: model.PhoneNumber, + PlanType: string(model.PlanType), + CreatedAt: model.CreatedAt, + UpdatedAt: model.UpdatedAt, + } +} + +func OrganizationEntityToResponse(entity *entities.Organization) *models.OrganizationResponse { + if entity == nil { + return nil + } + + return &models.OrganizationResponse{ + ID: entity.ID, + Name: entity.Name, + Email: entity.Email, + PhoneNumber: entity.PhoneNumber, + PlanType: constants.PlanType(entity.PlanType), + CreatedAt: entity.CreatedAt, + UpdatedAt: entity.UpdatedAt, + } +} + +func OrganizationEntitiesToModels(entities []*entities.Organization) []*models.Organization { + if entities == nil { + return nil + } + + models := make([]*models.Organization, len(entities)) + for i, entity := range entities { + models[i] = OrganizationEntityToModel(entity) + } + return models +} + +func OrganizationEntitiesToResponses(entities []*entities.Organization) []*models.OrganizationResponse { + if entities == nil { + return nil + } + + responses := make([]*models.OrganizationResponse, len(entities)) + for i, entity := range entities { + responses[i] = OrganizationEntityToResponse(entity) + } + return responses +} diff --git a/internal/mappers/outlet_mapper.go b/internal/mappers/outlet_mapper.go new file mode 100644 index 0000000..85ab08e --- /dev/null +++ b/internal/mappers/outlet_mapper.go @@ -0,0 +1,37 @@ +package mappers + +import ( + "apskel-pos-be/internal/entities" + "apskel-pos-be/internal/models" +) + +func OutletEntityToResponse(entity *entities.Outlet) *models.OutletResponse { + if entity == nil { + return nil + } + + return &models.OutletResponse{ + ID: entity.ID, + OrganizationID: entity.OrganizationID, + Name: entity.Name, + Address: entity.Address, + Timezone: entity.Timezone, + Currency: entity.Currency, + TaxRate: entity.TaxRate, + IsActive: entity.IsActive, + CreatedAt: entity.CreatedAt, + UpdatedAt: entity.UpdatedAt, + } +} + +func OutletEntitiesToResponses(entities []*entities.Outlet) []*models.OutletResponse { + if entities == nil { + return nil + } + + responses := make([]*models.OutletResponse, len(entities)) + for i, entity := range entities { + responses[i] = OutletEntityToResponse(entity) + } + return responses +} diff --git a/internal/mappers/payment_method_mapper.go b/internal/mappers/payment_method_mapper.go new file mode 100644 index 0000000..d4c8f5e --- /dev/null +++ b/internal/mappers/payment_method_mapper.go @@ -0,0 +1,179 @@ +package mappers + +import ( + "apskel-pos-be/internal/constants" + "apskel-pos-be/internal/contract" + "apskel-pos-be/internal/entities" + "apskel-pos-be/internal/models" +) + +func CreatePaymentMethodRequestToEntity(req *models.CreatePaymentMethodRequest) *entities.PaymentMethod { + paymentMethod := &entities.PaymentMethod{ + OrganizationID: req.OrganizationID, + Name: req.Name, + Type: entities.PaymentMethodType(req.Type), + Processor: req.Processor, + Configuration: entities.Metadata(req.Configuration), + IsActive: true, // Default to active + } + + if req.IsActive != nil { + paymentMethod.IsActive = *req.IsActive + } + + return paymentMethod +} + +func UpdatePaymentMethodEntityFromRequest(entity *entities.PaymentMethod, req *models.UpdatePaymentMethodRequest) { + if req.Name != nil { + entity.Name = *req.Name + } + if req.Type != nil { + entity.Type = entities.PaymentMethodType(*req.Type) + } + if req.Processor != nil { + entity.Processor = req.Processor + } + if req.Configuration != nil { + entity.Configuration = entities.Metadata(req.Configuration) + } + if req.IsActive != nil { + entity.IsActive = *req.IsActive + } +} + +func PaymentMethodEntityToResponse(entity *entities.PaymentMethod) *models.PaymentMethodResponse { + if entity == nil { + return nil + } + + return &models.PaymentMethodResponse{ + ID: entity.ID, + OrganizationID: entity.OrganizationID, + Name: entity.Name, + Type: constants.PaymentMethodType(entity.Type), + Processor: entity.Processor, + Configuration: map[string]interface{}(entity.Configuration), + IsActive: entity.IsActive, + CreatedAt: entity.CreatedAt, + UpdatedAt: entity.UpdatedAt, + } +} + +func PaymentMethodEntityToModel(entity *entities.PaymentMethod) *models.PaymentMethod { + if entity == nil { + return nil + } + + return &models.PaymentMethod{ + ID: entity.ID, + OrganizationID: entity.OrganizationID, + Name: entity.Name, + Type: constants.PaymentMethodType(entity.Type), + Processor: entity.Processor, + Configuration: map[string]interface{}(entity.Configuration), + IsActive: entity.IsActive, + CreatedAt: entity.CreatedAt, + UpdatedAt: entity.UpdatedAt, + } +} + +func PaymentMethodModelToEntity(model *models.PaymentMethod) *entities.PaymentMethod { + if model == nil { + return nil + } + + return &entities.PaymentMethod{ + ID: model.ID, + OrganizationID: model.OrganizationID, + Name: model.Name, + Type: entities.PaymentMethodType(model.Type), + Processor: model.Processor, + Configuration: entities.Metadata(model.Configuration), + IsActive: model.IsActive, + CreatedAt: model.CreatedAt, + UpdatedAt: model.UpdatedAt, + } +} + +// Contract to Model mappers +func CreatePaymentMethodContractToModel(req *contract.CreatePaymentMethodRequest) *models.CreatePaymentMethodRequest { + return &models.CreatePaymentMethodRequest{ + OrganizationID: req.OrganizationID, + Name: req.Name, + Type: constants.PaymentMethodType(req.Type), + Processor: req.Processor, + Configuration: req.Configuration, + IsActive: req.IsActive, + } +} + +func UpdatePaymentMethodContractToModel(req *contract.UpdatePaymentMethodRequest) *models.UpdatePaymentMethodRequest { + var paymentMethodType *constants.PaymentMethodType + if req.Type != nil { + pmt := constants.PaymentMethodType(*req.Type) + paymentMethodType = &pmt + } + + return &models.UpdatePaymentMethodRequest{ + Name: req.Name, + Type: paymentMethodType, + Processor: req.Processor, + Configuration: req.Configuration, + IsActive: req.IsActive, + } +} + +func ListPaymentMethodsContractToModel(req *contract.ListPaymentMethodsRequest) *models.ListPaymentMethodsRequest { + var paymentMethodType *constants.PaymentMethodType + if req.Type != nil { + pmt := constants.PaymentMethodType(*req.Type) + paymentMethodType = &pmt + } + + return &models.ListPaymentMethodsRequest{ + OrganizationID: req.OrganizationID, + Type: paymentMethodType, + IsActive: req.IsActive, + Search: req.Search, + Page: req.Page, + Limit: req.Limit, + } +} + +// Model to Contract mappers +func PaymentMethodResponseToContract(response *models.PaymentMethodResponse) *contract.PaymentMethodResponse { + if response == nil { + return nil + } + + return &contract.PaymentMethodResponse{ + ID: response.ID, + OrganizationID: response.OrganizationID, + Name: response.Name, + Type: string(response.Type), + Processor: response.Processor, + Configuration: response.Configuration, + IsActive: response.IsActive, + CreatedAt: response.CreatedAt, + UpdatedAt: response.UpdatedAt, + } +} + +func ListPaymentMethodsResponseToContract(response *models.ListPaymentMethodsResponse) *contract.ListPaymentMethodsResponse { + paymentMethods := make([]contract.PaymentMethodResponse, len(response.PaymentMethods)) + for i, pm := range response.PaymentMethods { + contractPM := PaymentMethodResponseToContract(&pm) + if contractPM != nil { + paymentMethods[i] = *contractPM + } + } + + return &contract.ListPaymentMethodsResponse{ + PaymentMethods: paymentMethods, + TotalCount: response.TotalCount, + Page: response.Page, + Limit: response.Limit, + TotalPages: response.TotalPages, + } +} diff --git a/internal/mappers/product_mapper.go b/internal/mappers/product_mapper.go new file mode 100644 index 0000000..61239bb --- /dev/null +++ b/internal/mappers/product_mapper.go @@ -0,0 +1,315 @@ +package mappers + +import ( + "apskel-pos-be/internal/constants" + "apskel-pos-be/internal/entities" + "apskel-pos-be/internal/models" +) + +func ProductEntityToModel(entity *entities.Product) *models.Product { + if entity == nil { + return nil + } + + return &models.Product{ + ID: entity.ID, + OrganizationID: entity.OrganizationID, + CategoryID: entity.CategoryID, + SKU: entity.SKU, + Name: entity.Name, + Description: entity.Description, + Price: entity.Price, + Cost: entity.Cost, + BusinessType: constants.BusinessType(entity.BusinessType), + Metadata: map[string]interface{}(entity.Metadata), + IsActive: entity.IsActive, + CreatedAt: entity.CreatedAt, + UpdatedAt: entity.UpdatedAt, + } +} + +func ProductModelToEntity(model *models.Product) *entities.Product { + if model == nil { + return nil + } + + return &entities.Product{ + ID: model.ID, + OrganizationID: model.OrganizationID, + CategoryID: model.CategoryID, + SKU: model.SKU, + Name: model.Name, + Description: model.Description, + Price: model.Price, + Cost: model.Cost, + BusinessType: string(model.BusinessType), + Metadata: entities.Metadata(model.Metadata), + IsActive: model.IsActive, + CreatedAt: model.CreatedAt, + UpdatedAt: model.UpdatedAt, + } +} + +func CreateProductRequestToEntity(req *models.CreateProductRequest) *entities.Product { + if req == nil { + return nil + } + + cost := float64(0) + if req.Cost > 0 { + cost = req.Cost + } + + businessType := "restaurant" + if req.BusinessType != "" { + businessType = string(req.BusinessType) + } + + metadata := entities.Metadata{} + if req.Metadata != nil { + metadata = entities.Metadata(req.Metadata) + } + + return &entities.Product{ + OrganizationID: req.OrganizationID, + CategoryID: req.CategoryID, + SKU: req.SKU, + Name: req.Name, + Description: req.Description, + Price: req.Price, + Cost: cost, + BusinessType: businessType, + Metadata: metadata, + IsActive: true, // Default to active + } +} + +func ProductEntityToResponse(entity *entities.Product) *models.ProductResponse { + if entity == nil { + return nil + } + + // Convert variants to response models + var variantResponses []models.ProductVariantResponse + if entity.ProductVariants != nil { + variantResponses = make([]models.ProductVariantResponse, len(entity.ProductVariants)) + for i, variant := range entity.ProductVariants { + variantResponses[i] = models.ProductVariantResponse{ + ID: variant.ID, + ProductID: variant.ProductID, + Name: variant.Name, + PriceModifier: variant.PriceModifier, + Cost: variant.Cost, + Metadata: map[string]interface{}(variant.Metadata), + CreatedAt: variant.CreatedAt, + UpdatedAt: variant.UpdatedAt, + } + } + } + + return &models.ProductResponse{ + ID: entity.ID, + OrganizationID: entity.OrganizationID, + CategoryID: entity.CategoryID, + SKU: entity.SKU, + Name: entity.Name, + Description: entity.Description, + Price: entity.Price, + Cost: entity.Cost, + BusinessType: constants.BusinessType(entity.BusinessType), + Metadata: map[string]interface{}(entity.Metadata), + IsActive: entity.IsActive, + CreatedAt: entity.CreatedAt, + UpdatedAt: entity.UpdatedAt, + Variants: variantResponses, + } +} + +func UpdateProductEntityFromRequest(entity *entities.Product, req *models.UpdateProductRequest) { + if entity == nil || req == nil { + return + } + + if req.CategoryID != nil { + entity.CategoryID = *req.CategoryID + } + + if req.SKU != nil { + entity.SKU = req.SKU + } + + if req.Name != nil { + entity.Name = *req.Name + } + + if req.Description != nil { + entity.Description = req.Description + } + + if req.Price != nil { + entity.Price = *req.Price + } + + if req.Cost != nil { + entity.Cost = *req.Cost + } + + if req.Metadata != nil { + if entity.Metadata == nil { + entity.Metadata = make(entities.Metadata) + } + for k, v := range req.Metadata { + entity.Metadata[k] = v + } + } + + if req.IsActive != nil { + entity.IsActive = *req.IsActive + } +} + +func ProductEntitiesToModels(entities []*entities.Product) []*models.Product { + if entities == nil { + return nil + } + + models := make([]*models.Product, len(entities)) + for i, entity := range entities { + models[i] = ProductEntityToModel(entity) + } + return models +} + +func ProductEntitiesToResponses(entities []*entities.Product) []*models.ProductResponse { + if entities == nil { + return nil + } + + responses := make([]*models.ProductResponse, len(entities)) + for i, entity := range entities { + responses[i] = ProductEntityToResponse(entity) + } + return responses +} + +// Product Variant Mappers +func ProductVariantEntityToModel(entity *entities.ProductVariant) *models.ProductVariant { + if entity == nil { + return nil + } + + return &models.ProductVariant{ + ID: entity.ID, + ProductID: entity.ProductID, + Name: entity.Name, + PriceModifier: entity.PriceModifier, + Cost: entity.Cost, + Metadata: map[string]interface{}(entity.Metadata), + CreatedAt: entity.CreatedAt, + UpdatedAt: entity.UpdatedAt, + } +} + +func ProductVariantModelToEntity(model *models.ProductVariant) *entities.ProductVariant { + if model == nil { + return nil + } + + return &entities.ProductVariant{ + ID: model.ID, + ProductID: model.ProductID, + Name: model.Name, + PriceModifier: model.PriceModifier, + Cost: model.Cost, + Metadata: entities.Metadata(model.Metadata), + CreatedAt: model.CreatedAt, + UpdatedAt: model.UpdatedAt, + } +} + +func CreateProductVariantRequestToEntity(req *models.CreateProductVariantRequest) *entities.ProductVariant { + if req == nil { + return nil + } + + metadata := entities.Metadata{} + if req.Metadata != nil { + metadata = entities.Metadata(req.Metadata) + } + + return &entities.ProductVariant{ + ProductID: req.ProductID, + Name: req.Name, + PriceModifier: req.PriceModifier, + Cost: req.Cost, + Metadata: metadata, + } +} + +func ProductVariantEntityToResponse(entity *entities.ProductVariant) *models.ProductVariantResponse { + if entity == nil { + return nil + } + + return &models.ProductVariantResponse{ + ID: entity.ID, + ProductID: entity.ProductID, + Name: entity.Name, + PriceModifier: entity.PriceModifier, + Cost: entity.Cost, + Metadata: map[string]interface{}(entity.Metadata), + CreatedAt: entity.CreatedAt, + UpdatedAt: entity.UpdatedAt, + } +} + +func UpdateProductVariantEntityFromRequest(entity *entities.ProductVariant, req *models.UpdateProductVariantRequest) { + if entity == nil || req == nil { + return + } + + if req.Name != nil { + entity.Name = *req.Name + } + + if req.PriceModifier != nil { + entity.PriceModifier = *req.PriceModifier + } + + if req.Cost != nil { + entity.Cost = *req.Cost + } + + if req.Metadata != nil { + if entity.Metadata == nil { + entity.Metadata = make(entities.Metadata) + } + for k, v := range req.Metadata { + entity.Metadata[k] = v + } + } +} + +func ProductVariantEntitiesToModels(entities []*entities.ProductVariant) []*models.ProductVariant { + if entities == nil { + return nil + } + + models := make([]*models.ProductVariant, len(entities)) + for i, entity := range entities { + models[i] = ProductVariantEntityToModel(entity) + } + return models +} + +func ProductVariantEntitiesToResponses(entities []*entities.ProductVariant) []*models.ProductVariantResponse { + if entities == nil { + return nil + } + + responses := make([]*models.ProductVariantResponse, len(entities)) + for i, entity := range entities { + responses[i] = ProductVariantEntityToResponse(entity) + } + return responses +} diff --git a/internal/mappers/user_mapper.go b/internal/mappers/user_mapper.go new file mode 100644 index 0000000..8fe8bd7 --- /dev/null +++ b/internal/mappers/user_mapper.go @@ -0,0 +1,110 @@ +package mappers + +import ( + "apskel-pos-be/internal/constants" + "apskel-pos-be/internal/entities" + "apskel-pos-be/internal/models" +) + +func UserEntityToModel(entity *entities.User) *models.User { + if entity == nil { + return nil + } + + return &models.User{ + ID: entity.ID, + OrganizationID: entity.OrganizationID, + OutletID: entity.OutletID, + Name: entity.Name, + Email: entity.Email, + Role: constants.UserRole(entity.Role), + Permissions: map[string]interface{}(entity.Permissions), + IsActive: entity.IsActive, + CreatedAt: entity.CreatedAt, + UpdatedAt: entity.UpdatedAt, + } +} + +func UserModelToEntity(model *models.User) *entities.User { + if model == nil { + return nil + } + + return &entities.User{ + ID: model.ID, + OrganizationID: model.OrganizationID, + OutletID: model.OutletID, + Name: model.Name, + Email: model.Email, + Role: entities.UserRole(model.Role), + Permissions: entities.Permissions(model.Permissions), + IsActive: model.IsActive, + CreatedAt: model.CreatedAt, + UpdatedAt: model.UpdatedAt, + } +} + +func UserCreateRequestToEntity(req *models.CreateUserRequest, passwordHash string) *entities.User { + if req == nil { + return nil + } + + permissions := entities.Permissions{} + if req.Permissions != nil { + permissions = entities.Permissions(req.Permissions) + } + + return &entities.User{ + OrganizationID: req.OrganizationID, + OutletID: req.OutletID, + Name: req.Name, + Email: req.Email, + PasswordHash: passwordHash, + Role: entities.UserRole(req.Role), + Permissions: permissions, + IsActive: true, + } +} + +func UserEntityToResponse(entity *entities.User) *models.UserResponse { + if entity == nil { + return nil + } + + return &models.UserResponse{ + ID: entity.ID, + OrganizationID: entity.OrganizationID, + OutletID: entity.OutletID, + Name: entity.Name, + Email: entity.Email, + Role: constants.UserRole(entity.Role), + Permissions: map[string]interface{}(entity.Permissions), + IsActive: entity.IsActive, + CreatedAt: entity.CreatedAt, + UpdatedAt: entity.UpdatedAt, + } +} + +func UserEntitiesToModels(entities []*entities.User) []*models.User { + if entities == nil { + return nil + } + + models := make([]*models.User, len(entities)) + for i, entity := range entities { + models[i] = UserEntityToModel(entity) + } + return models +} + +func UserEntitiesToResponses(entities []*entities.User) []*models.UserResponse { + if entities == nil { + return nil + } + + responses := make([]*models.UserResponse, len(entities)) + for i, entity := range entities { + responses[i] = UserEntityToResponse(entity) + } + return responses +} diff --git a/internal/middleware/auth_middleware.go b/internal/middleware/auth_middleware.go new file mode 100644 index 0000000..b43127a --- /dev/null +++ b/internal/middleware/auth_middleware.go @@ -0,0 +1,141 @@ +package middleware + +import ( + "apskel-pos-be/internal/appcontext" + "net/http" + "strings" + + "apskel-pos-be/internal/constants" + "apskel-pos-be/internal/contract" + "apskel-pos-be/internal/logger" + "apskel-pos-be/internal/service" + + "github.com/gin-gonic/gin" +) + +type AuthMiddleware struct { + authService service.AuthService +} + +func NewAuthMiddleware(authService service.AuthService) *AuthMiddleware { + return &AuthMiddleware{ + authService: authService, + } +} + +func (m *AuthMiddleware) RequireAuth() gin.HandlerFunc { + return func(c *gin.Context) { + token := m.extractTokenFromHeader(c) + if token == "" { + logger.FromContext(c.Request.Context()).Error("AuthMiddleware::RequireAuth -> Missing authorization token") + m.sendErrorResponse(c, "Authorization token is required", http.StatusUnauthorized) + c.Abort() + return + } + + userResponse, err := m.authService.ValidateToken(token) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("AuthMiddleware::RequireAuth -> Invalid token") + m.sendErrorResponse(c, "Invalid or expired token", http.StatusUnauthorized) + c.Abort() + return + } + + setKeyInContext(c, appcontext.UserRoleKey, userResponse.Role) + setKeyInContext(c, appcontext.OrganizationIDKey, userResponse.OrganizationID.String()) + setKeyInContext(c, appcontext.UserIDKey, userResponse.ID.String()) + + logger.FromContext(c.Request.Context()).Infof("AuthMiddleware::RequireAuth -> User authenticated: %s", userResponse.Email) + c.Next() + } +} + +func (m *AuthMiddleware) RequireRole(allowedRoles ...string) gin.HandlerFunc { + return func(c *gin.Context) { + appCtx := appcontext.FromGinContext(c.Request.Context()) + + hasRequiredRole := false + for _, role := range allowedRoles { + if appCtx.UserRole == role { + hasRequiredRole = true + break + } + } + + if !hasRequiredRole { + m.sendErrorResponse(c, "Insufficient permissions", http.StatusForbidden) + c.Abort() + return + } + + c.Next() + } +} + +func (m *AuthMiddleware) RequireAdminOrManager() gin.HandlerFunc { + return m.RequireRole("admin", "manager") +} + +func (m *AuthMiddleware) RequireAdmin() gin.HandlerFunc { + return m.RequireRole("admin") +} + +func (m *AuthMiddleware) RequireSuperAdmin() gin.HandlerFunc { + return m.RequireRole("superadmin") +} + +func (m *AuthMiddleware) RequireActiveUser() gin.HandlerFunc { + return func(c *gin.Context) { + userResponse, exists := c.Get("user") + if !exists { + logger.FromContext(c.Request.Context()).Error("AuthMiddleware::RequireActiveUser -> User not authenticated") + m.sendErrorResponse(c, "Authentication required", http.StatusUnauthorized) + c.Abort() + return + } + + user, ok := userResponse.(*contract.UserResponse) + if !ok { + logger.FromContext(c.Request.Context()).Error("AuthMiddleware::RequireActiveUser -> Invalid user context") + m.sendErrorResponse(c, "Invalid user context", http.StatusInternalServerError) + c.Abort() + return + } + + if !user.IsActive { + logger.FromContext(c.Request.Context()).Errorf("AuthMiddleware::RequireActiveUser -> User account is deactivated: %s", user.Email) + m.sendErrorResponse(c, "User account is deactivated", http.StatusForbidden) + c.Abort() + return + } + + logger.FromContext(c.Request.Context()).Infof("AuthMiddleware::RequireActiveUser -> Active user check passed: %s", user.Email) + c.Next() + } +} + +func (m *AuthMiddleware) extractTokenFromHeader(c *gin.Context) string { + authHeader := c.GetHeader("Authorization") + if authHeader == "" { + return "" + } + + parts := strings.Split(authHeader, " ") + if len(parts) != 2 || parts[0] != "Bearer" { + return "" + } + + return parts[1] +} + +func (m *AuthMiddleware) sendErrorResponse(c *gin.Context, message string, statusCode int) { + errorResponse := &contract.ErrorResponse{ + Error: "auth_error", + Message: message, + Code: statusCode, + Details: map[string]interface{}{ + "entity": constants.AuthHandlerEntity, + }, + } + c.JSON(statusCode, errorResponse) +} diff --git a/internal/middleware/auth_processor.go b/internal/middleware/auth_processor.go new file mode 100644 index 0000000..916acfe --- /dev/null +++ b/internal/middleware/auth_processor.go @@ -0,0 +1,4 @@ +package middleware + +type AuthProcessor interface { +} diff --git a/internal/middleware/context.go b/internal/middleware/context.go new file mode 100644 index 0000000..56df298 --- /dev/null +++ b/internal/middleware/context.go @@ -0,0 +1,67 @@ +package middleware + +import ( + "apskel-pos-be/internal/appcontext" + "apskel-pos-be/internal/constants" + "context" + "github.com/gin-gonic/gin" +) + +func PopulateContext() gin.HandlerFunc { + return func(c *gin.Context) { + setKeyInContext(c, appcontext.AppIDKey, getAppID(c)) + setKeyInContext(c, appcontext.AppVersionKey, getAppVersion(c)) + setKeyInContext(c, appcontext.AppTypeKey, getAppType(c)) + setKeyInContext(c, appcontext.OrganizationIDKey, getOrganizationID(c)) + setKeyInContext(c, appcontext.OutletIDKey, getOutletID(c)) + setKeyInContext(c, appcontext.DeviceOSKey, getDeviceOS(c)) + setKeyInContext(c, appcontext.PlatformKey, getDevicePlatform(c)) + setKeyInContext(c, appcontext.UserLocaleKey, getUserLocale(c)) + c.Next() + } +} + +func getAppID(c *gin.Context) string { + return c.GetHeader(constants.XAppIDHeader) +} + +func getAppType(c *gin.Context) string { + return c.GetHeader(constants.XAppTypeHeader) +} + +func getAppVersion(c *gin.Context) string { + return c.GetHeader(constants.XAppVersionHeader) +} + +func getOrganizationID(c *gin.Context) string { + return c.GetHeader(constants.OrganizationID) +} + +func getOutletID(c *gin.Context) string { + return c.GetHeader(constants.OutletID) +} + +func getDeviceOS(c *gin.Context) string { + return c.GetHeader(constants.XDeviceOSHeader) +} + +func getDevicePlatform(c *gin.Context) string { + return c.GetHeader(constants.XPlatformHeader) +} + +func getUserLocale(c *gin.Context) string { + userLocale := c.GetHeader(constants.XUserLocaleHeader) + if userLocale == "" { + userLocale = c.GetHeader(constants.AcceptedLanguageHeader) + } + if userLocale == "" { + userLocale = c.GetHeader(constants.LocaleHeader) + } + return userLocale +} + +func setKeyInContext(c *gin.Context, contextKey interface{}, contextKeyValue string) { + ctx := context.WithValue(c.Request.Context(), + contextKey, contextKeyValue) + c.Request = c.Request.WithContext(ctx) +} diff --git a/internal/middleware/correlation_id.go b/internal/middleware/correlation_id.go new file mode 100644 index 0000000..af7e870 --- /dev/null +++ b/internal/middleware/correlation_id.go @@ -0,0 +1,22 @@ +package middleware + +import ( + "apskel-pos-be/internal/appcontext" + "apskel-pos-be/internal/constants" + "context" + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +func CorrelationID() gin.HandlerFunc { + return func(c *gin.Context) { + correlationID := c.GetHeader(constants.CorrelationIDHeader) + if correlationID == "" { + correlationID = uuid.New().String() + } + ctx := context.WithValue(c.Request.Context(), appcontext.CorrelationIDKey, correlationID) + c.Request = c.Request.WithContext(ctx) + c.Writer.Header().Set(constants.CorrelationIDHeader, correlationID) + c.Next() + } +} diff --git a/internal/middleware/cors.go b/internal/middleware/cors.go new file mode 100644 index 0000000..224079b --- /dev/null +++ b/internal/middleware/cors.go @@ -0,0 +1,21 @@ +package middleware + +import ( + "github.com/gin-gonic/gin" +) + +func CORS() gin.HandlerFunc { + return gin.HandlerFunc(func(c *gin.Context) { + c.Header("Access-Control-Allow-Origin", "*") + c.Header("Access-Control-Allow-Credentials", "true") + c.Header("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With") + c.Header("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE") + + if c.Request.Method == "OPTIONS" { + c.AbortWithStatus(204) + return + } + + c.Next() + }) +} diff --git a/internal/middleware/json.go b/internal/middleware/json.go new file mode 100644 index 0000000..1dfd0e7 --- /dev/null +++ b/internal/middleware/json.go @@ -0,0 +1,17 @@ +package middleware + +import ( + "github.com/gin-gonic/gin" +) + +const ( + contentTypeHeader = "Content-Type" + jsonContentType = "application/json" +) + +func JsonAPI() gin.HandlerFunc { + return func(c *gin.Context) { + c.Writer.Header().Set(contentTypeHeader, jsonContentType) + c.Next() + } +} diff --git a/internal/middleware/logging.go b/internal/middleware/logging.go new file mode 100644 index 0000000..a5ff917 --- /dev/null +++ b/internal/middleware/logging.go @@ -0,0 +1,24 @@ +package middleware + +import ( + "fmt" + "time" + + "github.com/gin-gonic/gin" +) + +func Logging() gin.HandlerFunc { + return gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string { + return fmt.Sprintf("%s - [%s] \"%s %s %s %d %s \"%s\" %s\"\n", + param.ClientIP, + param.TimeStamp.Format(time.RFC1123), + param.Method, + param.Path, + param.Request.Proto, + param.StatusCode, + param.Latency, + param.Request.UserAgent(), + param.ErrorMessage, + ) + }) +} diff --git a/internal/middleware/rate_limit.go b/internal/middleware/rate_limit.go new file mode 100644 index 0000000..1186e64 --- /dev/null +++ b/internal/middleware/rate_limit.go @@ -0,0 +1,69 @@ +package middleware + +import ( + "sync" + "time" + + "github.com/gin-gonic/gin" +) + +type RateLimiter struct { + requests map[string][]time.Time + mutex sync.RWMutex + limit int + window time.Duration +} + +func NewRateLimiter(limit int, window time.Duration) *RateLimiter { + return &RateLimiter{ + requests: make(map[string][]time.Time), + limit: limit, + window: window, + } +} + +func (rl *RateLimiter) Allow(key string) bool { + rl.mutex.Lock() + defer rl.mutex.Unlock() + + now := time.Now() + windowStart := now.Add(-rl.window) + + // Clean old requests + if times, exists := rl.requests[key]; exists { + var validTimes []time.Time + for _, t := range times { + if t.After(windowStart) { + validTimes = append(validTimes, t) + } + } + rl.requests[key] = validTimes + } + + // Check if limit exceeded + if len(rl.requests[key]) >= rl.limit { + return false + } + + // Add current request + rl.requests[key] = append(rl.requests[key], now) + return true +} + +func RateLimit() gin.HandlerFunc { + limiter := NewRateLimiter(100, time.Minute) // 100 requests per minute + + return gin.HandlerFunc(func(c *gin.Context) { + clientIP := c.ClientIP() + + if !limiter.Allow(clientIP) { + c.JSON(429, gin.H{ + "error": "Rate limit exceeded", + }) + c.Abort() + return + } + + c.Next() + }) +} diff --git a/internal/middleware/recover.go b/internal/middleware/recover.go new file mode 100644 index 0000000..4e557f4 --- /dev/null +++ b/internal/middleware/recover.go @@ -0,0 +1,30 @@ +package middleware + +import ( + "apskel-pos-be/internal/contract" + "apskel-pos-be/internal/logger" + "apskel-pos-be/internal/util" + "github.com/gin-gonic/gin" + "net/http" + "runtime/debug" +) + +func Recover() gin.HandlerFunc { + return func(c *gin.Context) { + defer func() { + if err := recover(); err != nil { + logger.NonContext.Errorf(nil, "Recovered from panic %v", map[string]interface{}{ + "stack_trace": string(debug.Stack()), + "error": err, + }) + debug.PrintStack() + errorResponse := contract.BuildErrorResponse([]*contract.ResponseError{ + contract.NewResponseError("900", "", string(debug.Stack())), + }) + util.WriteResponse(c.Writer, c.Request, *errorResponse, http.StatusInternalServerError, "Middleware::Recover") + c.Abort() + } + }() + c.Next() + } +} diff --git a/internal/middleware/stat_logger.go b/internal/middleware/stat_logger.go new file mode 100644 index 0000000..fc0b4d6 --- /dev/null +++ b/internal/middleware/stat_logger.go @@ -0,0 +1,35 @@ +package middleware + +import ( + "apskel-pos-be/internal/constants" + "apskel-pos-be/internal/logger" + "fmt" + "github.com/gin-gonic/gin" + "net/http" + "time" +) + +func HTTPStatLogger() gin.HandlerFunc { + return func(c *gin.Context) { + if c.Request.URL.Path == "/health" { + c.Next() + return + } + + start := time.Now() + c.Next() + duration := time.Since(start) + + status := c.Writer.Status() + + log := logger.NewContextLogger(c, "HTTPStatLogger") + log.Infof("CompletedHTTPRequest %v", map[string]string{ + constants.RequestMethod: c.Request.Method, + constants.RequestPath: c.Request.URL.Path, + constants.RequestURLQueryParam: c.Request.URL.RawQuery, + constants.ResponseStatusCode: fmt.Sprintf("%d", status), + constants.ResponseStatusText: http.StatusText(status), + constants.ResponseTimeTaken: fmt.Sprintf("%f", duration.Seconds()), + }) + } +} diff --git a/internal/middleware/user_id_resolver.go b/internal/middleware/user_id_resolver.go new file mode 100644 index 0000000..0ac77c1 --- /dev/null +++ b/internal/middleware/user_id_resolver.go @@ -0,0 +1,64 @@ +package middleware + +import ( + "apskel-pos-be/internal/logger" + "apskel-pos-be/internal/models" + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +type UserIDResolver struct { + userProcessor UserProcessor + authProcessor AuthProcessor +} + +func NewUserIDResolver(userProcessor UserProcessor, authProcessor AuthProcessor) *UserIDResolver { + return &UserIDResolver{ + userProcessor: userProcessor, + authProcessor: authProcessor, + } +} + +func (uir *UserIDResolver) Handle() gin.HandlerFunc { + return func(c *gin.Context) { + //tokenString := c.GetHeader("Authorization") + //if tokenString == "" { + // c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header is required"}) + // c.Abort() + // return + //} + // + //ctx := c.Request.Context() + // + //user, err := uir.resolveUserID(c, gopayAccountID) + //if userID == "" { + // logger.FromContext(c.Request.Context()).Error("UserIDResolver::Handle -> userID could not be resolved") + // errorResponse := contract.BuildErrorResponse([]*contract.ResponseError{ + // contract.NewResponseError(constants.InternalServerErrorCode, "user-id", "failed to resolve user id from gopay account id"), + // }) + // util.WriteResponse(c.Writer, c.Request, *errorResponse, http.StatusInternalServerError, "UserIDResolver::Handle") + // c.Abort() + // return + //} + // + //setKeyInContext(c, appcontext.UserIDKey, user.ID.String()) + //setKeyInContext(c, appcontext.OrganizationIDKey, user.OrganizationID.String()) + //setKeyInContext(c, appcontext.OutletIDKey, user.OutletID.String()) + //setKeyInContext(c, appcontext.RoleIDKey, string(user.Role)) + + c.Next() + } +} + +func (uir *UserIDResolver) resolveUserID(c *gin.Context, userID uuid.UUID) (*models.UserResponse, error) { + user, err := uir.userProcessor.GetUserByID(c.Request.Context(), userID) + if err != nil { + logger.FromContext(c.Request.Context()).WithError(err).Error("UserIDResolver::resolveGopayUserID -> userID could not be resolved") + return nil, err + } + return user, nil +} + +func (uir *UserIDResolver) validate(c *gin.Context, tokenString string) string { + return "" +} diff --git a/internal/middleware/user_processor.go b/internal/middleware/user_processor.go new file mode 100644 index 0000000..2c2a221 --- /dev/null +++ b/internal/middleware/user_processor.go @@ -0,0 +1,11 @@ +package middleware + +import ( + "apskel-pos-be/internal/models" + "context" + "github.com/google/uuid" +) + +type UserProcessor interface { + GetUserByID(ctx context.Context, id uuid.UUID) (*models.UserResponse, error) +} diff --git a/internal/middlewares/auth.go b/internal/middlewares/auth.go deleted file mode 100644 index c1f19b5..0000000 --- a/internal/middlewares/auth.go +++ /dev/null @@ -1,141 +0,0 @@ -package middlewares - -import ( - "enaklo-pos-be/internal/common/mycontext" - "net/http" - "strings" - - "github.com/gin-gonic/gin" - - "enaklo-pos-be/internal/repository" -) - -func AuthorizationMiddleware(cryp repository.Crypto) gin.HandlerFunc { - return func(c *gin.Context) { - tokenString := c.GetHeader("Authorization") - if tokenString == "" { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header is required"}) - c.Abort() - return - } - - tokenString = strings.TrimPrefix(tokenString, "Bearer ") - - claims, err := cryp.ParseAndValidateJWT(tokenString) - if err != nil { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid JWT token"}) - c.Abort() - return - } - - customCtx, err := mycontext.NewMyContext(c.Request.Context(), claims) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "error initialize context"}) - c.Abort() - return - } - - c.Set("myCtx", customCtx) - - c.Next() - } -} - -func CustomerAuthorizationMiddleware(cryp repository.Crypto) gin.HandlerFunc { - return func(c *gin.Context) { - tokenString := c.GetHeader("Authorization") - if tokenString == "" { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header is required"}) - c.Abort() - return - } - - tokenString = strings.TrimPrefix(tokenString, "Bearer ") - - claims, err := cryp.ParseAndValidateJWTCustomer(tokenString) - if err != nil { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid JWT token"}) - c.Abort() - return - } - - customCtx, err := mycontext.NewMyContextCustomer(c.Request.Context(), claims) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "error initialize context"}) - c.Abort() - return - } - - c.Set("myCtx", customCtx) - - c.Next() - } -} - -func OptionalCustomerAuthorizationMiddleware(cryp repository.Crypto) gin.HandlerFunc { - return func(c *gin.Context) { - tokenString := c.GetHeader("Authorization") - if tokenString == "" { - c.Next() - return - } - - tokenString = strings.TrimPrefix(tokenString, "Bearer ") - - claims, err := cryp.ParseAndValidateJWTCustomer(tokenString) - if err != nil { - c.Next() - return - } - - customCtx, err := mycontext.NewMyContextCustomer(c.Request.Context(), claims) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "error initialize context"}) - c.Next() - return - } - - c.Set("myCtx", customCtx) - c.Next() - } -} - -func SuperAdminMiddleware() gin.HandlerFunc { - return func(c *gin.Context) { - ctx, exists := c.Get("myCtx") - if !exists { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) - c.Abort() - return - } - - myCtx, ok := ctx.(*mycontext.MyContextImpl) - if !ok || !myCtx.IsSuperAdmin() { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) - c.Abort() - return - } - - c.Next() - } -} - -func IsAdminMiddleware() gin.HandlerFunc { - return func(c *gin.Context) { - ctx, exists := c.Get("myCtx") - if !exists { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) - c.Abort() - return - } - - myCtx, ok := ctx.(*mycontext.MyContextImpl) - if !ok || !myCtx.IsAdmin() { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) - c.Abort() - return - } - - c.Next() - } -} diff --git a/internal/middlewares/cors.go b/internal/middlewares/cors.go deleted file mode 100644 index 586ca10..0000000 --- a/internal/middlewares/cors.go +++ /dev/null @@ -1,38 +0,0 @@ -package middlewares - -import ( - "fmt" - "net/http" - - "github.com/gin-gonic/gin" - - "enaklo-pos-be/internal/common/logger" -) - -func Cors() gin.HandlerFunc { - return func(c *gin.Context) { - c.Header("Access-Control-Allow-Origin", "*") - c.Header("Access-Control-Allow-Credentials", "true") - c.Header("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, Accept, origin, Referer, Cache-Control, X-Requested-With") - c.Header("Access-Control-Allow-Methods", "POST,HEAD,PATCH, OPTIONS, GET, PUT, DELETE") - c.Header("Vary", "Origin") - - if c.Request.Method == "OPTIONS" { - c.AbortWithStatus(204) - return - } - - c.Next() - } -} - -func LogCorsError() gin.HandlerFunc { - return func(c *gin.Context) { - c.Next() - - // check if the request was blocked due to CORS - if c.Writer.Status() == http.StatusForbidden && c.Writer.Header().Get("Access-Control-Allow-Origin") == "" { - logger.GetLogger().Error(fmt.Sprintf("CORS error: %s", c.Writer.Header().Get("Access-Control-Allow-Origin"))) - } - } -} diff --git a/internal/middlewares/logger.go b/internal/middlewares/logger.go deleted file mode 100644 index 9d1ff22..0000000 --- a/internal/middlewares/logger.go +++ /dev/null @@ -1,43 +0,0 @@ -package middlewares - -import ( - "fmt" - "time" - - "github.com/gin-gonic/gin" - - "enaklo-pos-be/internal/common/request" -) - -type ConfigLogger interface { - IsLoggerEnabled() bool -} - -func Logger(c ConfigLogger) gin.HandlerFunc { - if !c.IsLoggerEnabled() { - return func(ctx *gin.Context) { - ctx.Next() - } - } - - return gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string { - var parsedReqInfo request.RequestInfo - - if reqInfo, exists := param.Keys[request.ReqInfoKey]; exists { - if castedReqInfo, ok := reqInfo.(request.RequestInfo); ok { - parsedReqInfo = castedReqInfo - } - } - - return fmt.Sprintf( - "%s - [HTTP] TraceId: %s; UserId: %d; Method: %s; Path: %s; Status: %d; Latency: %s;\n\n", - param.TimeStamp.Format(time.RFC1123), - parsedReqInfo.TraceId, - parsedReqInfo.UserId, - param.Method, - param.Path, - param.StatusCode, - param.Latency, - ) - }) -} diff --git a/internal/middlewares/request.go b/internal/middlewares/request.go deleted file mode 100644 index 796003e..0000000 --- a/internal/middlewares/request.go +++ /dev/null @@ -1,136 +0,0 @@ -package middlewares - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "net/http" - "strconv" - "strings" - "time" - - "github.com/gin-gonic/gin" - "go.uber.org/zap" - - "enaklo-pos-be/internal/common/logger" -) - -func RequestMiddleware(c ConfigLogger) (handler gin.HandlerFunc) { - if !c.IsLoggerEnabled() { - return func(ctx *gin.Context) { - ctx.Next() - } - } - - return func(ctx *gin.Context) { - start := time.Now() - body, _ := readRequestBody(ctx.Request) - reqData := getRequestParam(ctx.Request, body) - - // Check if the request contains a file - isFileUpload := false - contentType := ctx.Request.Header.Get("Content-Type") - if strings.HasPrefix(contentType, "multipart/form-data") { - isFileUpload = true - } - - // Log the request if it's not a file upload - if !isFileUpload { - logger.ContextLogger(ctx).With(reqData...).Info("Request") - } - - rbw := &ResponseBodyWriter{body: bytes.NewBufferString(""), ResponseWriter: ctx.Writer} - ctx.Writer = rbw - - stop := time.Now() - latency := stop.Sub(start).Milliseconds() - - resData := reqData - resData = append(resData, getResponseParam(rbw, latency)...) - - if !isFileUpload { - logger.ContextLogger(ctx).With(resData...).Info("Response") - } - } -} - -func readRequestBody(req *http.Request) ([]byte, error) { - body, err := io.ReadAll(req.Body) - if err != nil { - logger.ContextLogger(req.Context()).Error(fmt.Sprintf("Error reading body: %v", err)) - return nil, err - } - - req.Body = io.NopCloser(bytes.NewBuffer(body)) - - return body, nil -} - -type ResponseBodyWriter struct { - gin.ResponseWriter - body *bytes.Buffer -} - -func excludeSensitiveFields(data []interface{}) []interface{} { - var result []interface{} - for _, item := range data { - if param, ok := item.(gin.Param); ok { - // Exclude Authorization and Password fields - if param.Key != "Authorization" && param.Key != "Password" { - result = append(result, item) - } - } else { - result = append(result, item) - } - } - return result -} - -func getRequestParam(req *http.Request, body []byte) []zap.Field { - var reqData []zap.Field - reqData = append(reqData, zap.Any("host", req.Host), - zap.Any("uri", req.RequestURI), - zap.Any("method", req.Method), - zap.Any("path", func() interface{} { - p := req.URL.Path - if p == "" { - p = "/" - } - - return p - }()), - zap.Any("protocol", req.Proto), - zap.Any("referer", req.Referer()), - zap.Any("user_agent", req.UserAgent()), - zap.Any("headers", req.Header), - zap.Any("remote_ip", req.RemoteAddr), - zap.Any("body", excludeSensitiveFieldsFromBody(body)), - ) - - return reqData -} - -func getResponseParam(rbw *ResponseBodyWriter, latency int64) []zap.Field { - var resData []zap.Field - resData = append(resData, - zap.Any("httpStatus", rbw.Status()), - zap.Any("body", rbw.body.String()), - zap.Any("latency_human", strconv.FormatInt(latency, 10)), - zap.Any("headers", rbw.Header()), - ) - - return resData -} - -func excludeSensitiveFieldsFromBody(body []byte) string { - var data map[string]interface{} - if err := json.Unmarshal(body, &data); err != nil { - return string(body) - } - - delete(data, "password") - - result, _ := json.Marshal(data) - return string(result) -} diff --git a/internal/middlewares/trace.go b/internal/middlewares/trace.go deleted file mode 100644 index c5c835a..0000000 --- a/internal/middlewares/trace.go +++ /dev/null @@ -1,20 +0,0 @@ -package middlewares - -import ( - "enaklo-pos-be/internal/common/request" - "enaklo-pos-be/internal/constants" - "enaklo-pos-be/internal/utils/generator" - "github.com/gin-gonic/gin" -) - -func Trace() gin.HandlerFunc { - return func(c *gin.Context) { - traceId := c.Request.Header.Get("Trace-Id") - if traceId == "" { - traceId = generator.GenerateUUID() - } - - request.SetTraceId(c, traceId) - c.Set(constants.ContextRequestID, traceId) - } -} diff --git a/internal/models/analytics.go b/internal/models/analytics.go new file mode 100644 index 0000000..94443a0 --- /dev/null +++ b/internal/models/analytics.go @@ -0,0 +1,214 @@ +package models + +import ( + "time" + + "github.com/google/uuid" +) + +// PaymentMethodAnalyticsRequest represents the request for payment method analytics +type PaymentMethodAnalyticsRequest struct { + OrganizationID uuid.UUID `validate:"required"` + OutletID *uuid.UUID `validate:"omitempty"` + DateFrom time.Time `validate:"required"` + DateTo time.Time `validate:"required"` + GroupBy string `validate:"omitempty,oneof=day hour week month"` +} + +// PaymentMethodAnalyticsResponse represents the response for payment method analytics +type PaymentMethodAnalyticsResponse struct { + OrganizationID uuid.UUID `json:"organization_id"` + OutletID *uuid.UUID `json:"outlet_id,omitempty"` + DateFrom time.Time `json:"date_from"` + DateTo time.Time `json:"date_to"` + GroupBy string `json:"group_by"` + Summary PaymentMethodSummary `json:"summary"` + Data []PaymentMethodAnalyticsData `json:"data"` +} + +// PaymentMethodSummary represents the summary of payment method analytics +type PaymentMethodSummary struct { + TotalAmount float64 `json:"total_amount"` + TotalOrders int64 `json:"total_orders"` + TotalPayments int64 `json:"total_payments"` + AverageOrderValue float64 `json:"average_order_value"` +} + +// PaymentMethodAnalyticsData represents individual payment method analytics data +type PaymentMethodAnalyticsData struct { + PaymentMethodID uuid.UUID `json:"payment_method_id"` + PaymentMethodName string `json:"payment_method_name"` + PaymentMethodType string `json:"payment_method_type"` + TotalAmount float64 `json:"total_amount"` + OrderCount int64 `json:"order_count"` + PaymentCount int64 `json:"payment_count"` + Percentage float64 `json:"percentage"` +} + +// SalesAnalyticsRequest represents the request for sales analytics +type SalesAnalyticsRequest struct { + OrganizationID uuid.UUID `validate:"required"` + OutletID *uuid.UUID `validate:"omitempty"` + DateFrom time.Time `validate:"required"` + DateTo time.Time `validate:"required"` + GroupBy string `validate:"omitempty,oneof=day hour week month"` +} + +// SalesAnalyticsResponse represents the response for sales analytics +type SalesAnalyticsResponse struct { + OrganizationID uuid.UUID `json:"organization_id"` + OutletID *uuid.UUID `json:"outlet_id,omitempty"` + DateFrom time.Time `json:"date_from"` + DateTo time.Time `json:"date_to"` + GroupBy string `json:"group_by"` + Summary SalesSummary `json:"summary"` + Data []SalesAnalyticsData `json:"data"` +} + +// SalesSummary represents the summary of sales analytics +type SalesSummary struct { + TotalSales float64 `json:"total_sales"` + TotalOrders int64 `json:"total_orders"` + TotalItems int64 `json:"total_items"` + AverageOrderValue float64 `json:"average_order_value"` + TotalTax float64 `json:"total_tax"` + TotalDiscount float64 `json:"total_discount"` + NetSales float64 `json:"net_sales"` +} + +// SalesAnalyticsData represents individual sales analytics data point +type SalesAnalyticsData struct { + Date time.Time `json:"date"` + Sales float64 `json:"sales"` + Orders int64 `json:"orders"` + Items int64 `json:"items"` + Tax float64 `json:"tax"` + Discount float64 `json:"discount"` + NetSales float64 `json:"net_sales"` +} + +// ProductAnalyticsRequest represents the request for product analytics +type ProductAnalyticsRequest struct { + OrganizationID uuid.UUID `validate:"required"` + OutletID *uuid.UUID `validate:"omitempty"` + DateFrom time.Time `validate:"required"` + DateTo time.Time `validate:"required"` + Limit int `validate:"min=1,max=100"` +} + +// ProductAnalyticsResponse represents the response for product analytics +type ProductAnalyticsResponse struct { + OrganizationID uuid.UUID `json:"organization_id"` + OutletID *uuid.UUID `json:"outlet_id,omitempty"` + DateFrom time.Time `json:"date_from"` + DateTo time.Time `json:"date_to"` + Data []ProductAnalyticsData `json:"data"` +} + +// ProductAnalyticsData represents individual product analytics data +type ProductAnalyticsData struct { + ProductID uuid.UUID `json:"product_id"` + ProductName string `json:"product_name"` + CategoryID uuid.UUID `json:"category_id"` + CategoryName string `json:"category_name"` + QuantitySold int64 `json:"quantity_sold"` + Revenue float64 `json:"revenue"` + AveragePrice float64 `json:"average_price"` + OrderCount int64 `json:"order_count"` +} + +// DashboardAnalyticsRequest represents the request for dashboard analytics +type DashboardAnalyticsRequest struct { + OrganizationID uuid.UUID `validate:"required"` + OutletID *uuid.UUID `validate:"omitempty"` + DateFrom time.Time `validate:"required"` + DateTo time.Time `validate:"required"` +} + +// DashboardAnalyticsResponse represents the response for dashboard analytics +type DashboardAnalyticsResponse struct { + OrganizationID uuid.UUID `json:"organization_id"` + OutletID *uuid.UUID `json:"outlet_id,omitempty"` + DateFrom time.Time `json:"date_from"` + DateTo time.Time `json:"date_to"` + Overview DashboardOverview `json:"overview"` + TopProducts []ProductAnalyticsData `json:"top_products"` + PaymentMethods []PaymentMethodAnalyticsData `json:"payment_methods"` + RecentSales []SalesAnalyticsData `json:"recent_sales"` +} + +// DashboardOverview represents the overview data for dashboard +type DashboardOverview struct { + TotalSales float64 `json:"total_sales"` + TotalOrders int64 `json:"total_orders"` + AverageOrderValue float64 `json:"average_order_value"` + TotalCustomers int64 `json:"total_customers"` + VoidedOrders int64 `json:"voided_orders"` + RefundedOrders int64 `json:"refunded_orders"` +} + +// ProfitLossAnalyticsRequest represents the request for profit and loss analytics +type ProfitLossAnalyticsRequest struct { + OrganizationID uuid.UUID `validate:"required"` + OutletID *uuid.UUID `validate:"omitempty"` + DateFrom time.Time `validate:"required"` + DateTo time.Time `validate:"required"` + GroupBy string `validate:"omitempty,oneof=day hour week month"` +} + +// ProfitLossAnalyticsResponse represents the response for profit and loss analytics +type ProfitLossAnalyticsResponse struct { + OrganizationID uuid.UUID `json:"organization_id"` + OutletID *uuid.UUID `json:"outlet_id,omitempty"` + DateFrom time.Time `json:"date_from"` + DateTo time.Time `json:"date_to"` + GroupBy string `json:"group_by"` + Summary ProfitLossSummary `json:"summary"` + Data []ProfitLossData `json:"data"` + ProductData []ProductProfitData `json:"product_data"` +} + +// ProfitLossSummary represents the summary of profit and loss analytics +type ProfitLossSummary struct { + TotalRevenue float64 `json:"total_revenue"` + TotalCost float64 `json:"total_cost"` + GrossProfit float64 `json:"gross_profit"` + GrossProfitMargin float64 `json:"gross_profit_margin"` + TotalTax float64 `json:"total_tax"` + TotalDiscount float64 `json:"total_discount"` + NetProfit float64 `json:"net_profit"` + NetProfitMargin float64 `json:"net_profit_margin"` + TotalOrders int64 `json:"total_orders"` + AverageProfit float64 `json:"average_profit"` + ProfitabilityRatio float64 `json:"profitability_ratio"` +} + +// ProfitLossData represents individual profit and loss data point by time period +type ProfitLossData struct { + Date time.Time `json:"date"` + Revenue float64 `json:"revenue"` + Cost float64 `json:"cost"` + GrossProfit float64 `json:"gross_profit"` + GrossProfitMargin float64 `json:"gross_profit_margin"` + Tax float64 `json:"tax"` + Discount float64 `json:"discount"` + NetProfit float64 `json:"net_profit"` + NetProfitMargin float64 `json:"net_profit_margin"` + Orders int64 `json:"orders"` +} + +// ProductProfitData represents profit data for individual products +type ProductProfitData struct { + ProductID uuid.UUID `json:"product_id"` + ProductName string `json:"product_name"` + CategoryID uuid.UUID `json:"category_id"` + CategoryName string `json:"category_name"` + QuantitySold int64 `json:"quantity_sold"` + Revenue float64 `json:"revenue"` + Cost float64 `json:"cost"` + GrossProfit float64 `json:"gross_profit"` + GrossProfitMargin float64 `json:"gross_profit_margin"` + AveragePrice float64 `json:"average_price"` + AverageCost float64 `json:"average_cost"` + ProfitPerUnit float64 `json:"profit_per_unit"` +} diff --git a/internal/models/category.go b/internal/models/category.go new file mode 100644 index 0000000..2ff54dc --- /dev/null +++ b/internal/models/category.go @@ -0,0 +1,47 @@ +package models + +import ( + "time" + + "github.com/google/uuid" +) + +type Category struct { + ID uuid.UUID + OrganizationID uuid.UUID + Name string + Description *string + ImageURL *string + SortOrder int + IsActive bool + CreatedAt time.Time + UpdatedAt time.Time +} + +type CreateCategoryRequest struct { + OrganizationID uuid.UUID `validate:"required"` + Name string `validate:"required,min=1,max=255"` + Description *string `validate:"omitempty,max=1000"` + ImageURL *string `validate:"omitempty,url"` + SortOrder int `validate:"min=0"` +} + +type UpdateCategoryRequest struct { + Name *string `validate:"omitempty,min=1,max=255"` + Description *string `validate:"omitempty,max=1000"` + ImageURL *string `validate:"omitempty,url"` + SortOrder *int `validate:"omitempty,min=0"` + IsActive *bool +} + +type CategoryResponse struct { + ID uuid.UUID + OrganizationID uuid.UUID + Name string + Description *string + ImageURL *string + SortOrder int + IsActive bool + CreatedAt time.Time + UpdatedAt time.Time +} diff --git a/internal/models/customer.go b/internal/models/customer.go new file mode 100644 index 0000000..b873198 --- /dev/null +++ b/internal/models/customer.go @@ -0,0 +1,53 @@ +package models + +import ( + "apskel-pos-be/internal/entities" + "time" + + "github.com/google/uuid" +) + +type CreateCustomerRequest struct { + Name string `json:"name" validate:"required"` + Email *string `json:"email,omitempty" validate:"omitempty,email"` + Phone *string `json:"phone,omitempty"` + Address *string `json:"address,omitempty"` +} + +type UpdateCustomerRequest struct { + Name *string `json:"name,omitempty" validate:"omitempty,required"` + Email *string `json:"email,omitempty" validate:"omitempty,email"` + Phone *string `json:"phone,omitempty"` + Address *string `json:"address,omitempty"` + IsActive *bool `json:"is_active,omitempty"` +} + +type CustomerResponse struct { + ID uuid.UUID `json:"id"` + OrganizationID uuid.UUID `json:"organization_id"` + Name string `json:"name"` + Email *string `json:"email,omitempty"` + Phone *string `json:"phone,omitempty"` + Address *string `json:"address,omitempty"` + IsDefault bool `json:"is_default"` + IsActive bool `json:"is_active"` + Metadata entities.Metadata `json:"metadata"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// ListCustomersQuery represents query parameters for listing customers +type ListCustomersQuery struct { + Page int `query:"page" validate:"min=1"` + Limit int `query:"limit" validate:"min=1,max=100"` + Search string `query:"search"` + IsActive *bool `query:"is_active"` + IsDefault *bool `query:"is_default"` + SortBy string `query:"sort_by" validate:"omitempty,oneof=name email created_at updated_at"` + SortOrder string `query:"sort_order" validate:"omitempty,oneof=asc desc"` +} + +// SetDefaultCustomerRequest represents the request to set a customer as default +type SetDefaultCustomerRequest struct { + CustomerID uuid.UUID `json:"customer_id" validate:"required"` +} diff --git a/internal/models/file.go b/internal/models/file.go new file mode 100644 index 0000000..a7c088c --- /dev/null +++ b/internal/models/file.go @@ -0,0 +1,102 @@ +package models + +import ( + "apskel-pos-be/internal/constants" + "time" + + "github.com/google/uuid" +) + +type File struct { + ID uuid.UUID + OrganizationID uuid.UUID + UserID uuid.UUID + FileName string + OriginalName string + FileURL string + FileSize int64 + MimeType string + FileType string + UploadPath string + IsPublic bool + Metadata map[string]interface{} + CreatedAt time.Time + UpdatedAt time.Time +} + +// Request DTOs +type UploadFileRequest struct { + FileType constants.FileType `validate:"required"` + IsPublic *bool `validate:"omitempty"` + Metadata map[string]interface{} +} + +type UpdateFileRequest struct { + IsPublic *bool `validate:"omitempty"` + Metadata map[string]interface{} +} + +type ListFilesRequest struct { + OrganizationID *uuid.UUID + UserID *uuid.UUID + FileType *constants.FileType + IsPublic *bool + DateFrom *time.Time + DateTo *time.Time + Search string + Page int `validate:"required,min=1"` + Limit int `validate:"required,min=1,max=100"` +} + +// Response DTOs +type FileResponse struct { + ID uuid.UUID + OrganizationID uuid.UUID + UserID uuid.UUID + FileName string + OriginalName string + FileURL string + FileSize int64 + MimeType string + FileType string + UploadPath string + IsPublic bool + Metadata map[string]interface{} + CreatedAt time.Time + UpdatedAt time.Time +} + +type ListFilesResponse struct { + Files []*FileResponse + TotalCount int + Page int + Limit int + TotalPages int +} + +type UploadFileResponse struct { + File FileResponse +} + +// Helper methods +func (f *File) IsImage() bool { + return f.FileType == string(constants.FileTypeImage) +} + +func (f *File) IsDocument() bool { + return f.FileType == string(constants.FileTypeDocument) +} + +func (f *File) IsVideo() bool { + return f.FileType == string(constants.FileTypeVideo) +} + +func (f *File) GetFileExtension() string { + // Extract extension from original name + for i := len(f.OriginalName) - 1; i >= 0; i-- { + if f.OriginalName[i] == '.' { + return f.OriginalName[i:] + } + } + return "" +} diff --git a/internal/models/inventory.go b/internal/models/inventory.go new file mode 100644 index 0000000..46fff41 --- /dev/null +++ b/internal/models/inventory.go @@ -0,0 +1,59 @@ +package models + +import ( + "time" + + "github.com/google/uuid" +) + +type Inventory struct { + ID uuid.UUID + OutletID uuid.UUID + ProductID uuid.UUID + Quantity int + ReorderLevel int + UpdatedAt time.Time +} + +type CreateInventoryRequest struct { + OutletID uuid.UUID + ProductID uuid.UUID + Quantity int + ReorderLevel int +} + +type UpdateInventoryRequest struct { + Quantity *int + ReorderLevel *int +} + +type InventoryAdjustmentRequest struct { + Delta int + Reason string +} + +type InventoryResponse struct { + ID uuid.UUID + OutletID uuid.UUID + ProductID uuid.UUID + Quantity int + ReorderLevel int + IsLowStock bool + UpdatedAt time.Time +} + +func (i *Inventory) IsLowStock() bool { + return i.Quantity <= i.ReorderLevel +} + +func (i *Inventory) CanFulfillOrder(requestedQuantity int) bool { + return i.Quantity >= requestedQuantity +} + +func (i *Inventory) AdjustQuantity(delta int) int { + newQuantity := i.Quantity + delta + if newQuantity < 0 { + newQuantity = 0 + } + return newQuantity +} diff --git a/internal/models/models.go b/internal/models/models.go new file mode 100644 index 0000000..ce54994 --- /dev/null +++ b/internal/models/models.go @@ -0,0 +1,32 @@ +package models + +// Pagination represents pagination information +type Pagination struct { + Page int `json:"page"` + Limit int `json:"limit"` + Total int64 `json:"total"` + TotalPages int `json:"total_pages"` +} + +// PaginatedResponse represents a paginated response +type PaginatedResponse[T any] struct { + Data []T `json:"data"` + Pagination Pagination `json:"pagination"` +} + +func GetAllModelNames() []string { + return []string{ + "Organization", + "Outlet", + "User", + "Category", + "Product", + "ProductVariant", + "Inventory", + "Order", + "OrderItem", + "PaymentMethod", + "Payment", + "Customer", + } +} diff --git a/internal/models/order.go b/internal/models/order.go new file mode 100644 index 0000000..9e1ce87 --- /dev/null +++ b/internal/models/order.go @@ -0,0 +1,288 @@ +package models + +import ( + "apskel-pos-be/internal/constants" + "time" + + "github.com/google/uuid" +) + +type Order struct { + ID uuid.UUID + OrganizationID uuid.UUID + OutletID uuid.UUID + UserID uuid.UUID + CustomerID *uuid.UUID + OrderNumber string + TableNumber *string + OrderType constants.OrderType + Status constants.OrderStatus + Subtotal float64 + TaxAmount float64 + DiscountAmount float64 + TotalAmount float64 + TotalCost float64 + PaymentStatus constants.PaymentStatus + RefundAmount float64 + IsVoid bool + IsRefund bool + VoidReason *string + VoidedAt *time.Time + VoidedBy *uuid.UUID + RefundReason *string + RefundedAt *time.Time + RefundedBy *uuid.UUID + Metadata map[string]interface{} + CreatedAt time.Time + UpdatedAt time.Time +} + +type OrderItem struct { + ID uuid.UUID + OrderID uuid.UUID + ProductID uuid.UUID + ProductVariantID *uuid.UUID + Quantity int + UnitPrice float64 + TotalPrice float64 + UnitCost float64 + TotalCost float64 + RefundAmount float64 + RefundQuantity int + IsPartiallyRefunded bool + IsFullyRefunded bool + RefundReason *string + RefundedAt *time.Time + RefundedBy *uuid.UUID + Modifiers []map[string]interface{} + Status constants.OrderItemStatus + CreatedAt time.Time + UpdatedAt time.Time +} + +type PaymentOrderItem struct { + ID uuid.UUID + PaymentID uuid.UUID + OrderItemID uuid.UUID + Amount float64 + CreatedAt time.Time + UpdatedAt time.Time +} + +type OrderSequence struct { + ID uuid.UUID + OrganizationID uuid.UUID + OutletID uuid.UUID + Year int + Month int + SequenceNumber int + CreatedAt time.Time + UpdatedAt time.Time +} + +type CreateOrderRequest struct { + OutletID uuid.UUID `validate:"required"` + UserID uuid.UUID `validate:"required"` + CustomerID *uuid.UUID `validate:"omitempty"` + TableNumber *string `validate:"omitempty,max=50"` + OrderType constants.OrderType `validate:"required"` + OrderItems []CreateOrderItemRequest `validate:"required,min=1,dive"` + Notes *string `validate:"omitempty,max=1000"` + Metadata map[string]interface{} + CustomerName *string `validate:"omitempty,max=255"` +} + +type CreateOrderItemRequest struct { + ProductID uuid.UUID `validate:"required"` + ProductVariantID *uuid.UUID `validate:"omitempty"` + Quantity int `validate:"required,min=1"` + UnitPrice *float64 `validate:"omitempty,min=0"` // Optional, will use database price if not provided + Modifiers []map[string]interface{} `validate:"omitempty"` + Notes *string `validate:"omitempty,max=500"` + Metadata map[string]interface{} `validate:"omitempty"` +} + +type UpdateOrderRequest struct { + TableNumber *string `validate:"omitempty,max=50"` + Status *constants.OrderStatus `validate:"omitempty"` + DiscountAmount *float64 `validate:"omitempty,min=0"` + Notes *string `validate:"omitempty,max=1000"` + Metadata map[string]interface{} +} + +type CreatePaymentOrderItemRequest struct { + OrderItemID uuid.UUID `validate:"required"` + Amount float64 `validate:"required,min=0"` +} + +type VoidOrderRequest struct { + OrderID uuid.UUID `validate:"required"` + Reason string `validate:"required"` + Type string `validate:"required,oneof=ALL ITEM"` + Items []VoidItemRequest `validate:"required_if=Type ITEM,dive"` +} + +type VoidItemRequest struct { + OrderItemID uuid.UUID `validate:"required"` + Quantity int `validate:"required,min=1"` +} + +type RefundOrderRequest struct { + Reason *string `validate:"omitempty,max=255"` + RefundAmount *float64 `validate:"omitempty,min=0"` + OrderItems []RefundOrderItemRequest `validate:"omitempty,dive"` +} + +type RefundOrderItemRequest struct { + OrderItemID uuid.UUID `validate:"required"` + RefundQuantity int `validate:"omitempty,min=1"` + RefundAmount *float64 `validate:"omitempty,min=0"` + Reason *string `validate:"omitempty,max=255"` +} + +// Response DTOs +type OrderResponse struct { + ID uuid.UUID + OrganizationID uuid.UUID + OutletID uuid.UUID + UserID uuid.UUID + CustomerID *uuid.UUID + OrderNumber string + TableNumber *string + OrderType constants.OrderType + Status constants.OrderStatus + Subtotal float64 + TaxAmount float64 + DiscountAmount float64 + TotalAmount float64 + TotalCost float64 + PaymentStatus constants.PaymentStatus + RefundAmount float64 + IsVoid bool + IsRefund bool + VoidReason *string + VoidedAt *time.Time + VoidedBy *uuid.UUID + RefundReason *string + RefundedAt *time.Time + RefundedBy *uuid.UUID + Notes *string + Metadata map[string]interface{} + CreatedAt time.Time + UpdatedAt time.Time + OrderItems []OrderItemResponse + Payments []PaymentResponse +} + +type OrderItemResponse struct { + ID uuid.UUID + OrderID uuid.UUID + ProductID uuid.UUID + ProductName string + ProductVariantID *uuid.UUID + ProductVariantName *string + Quantity int + UnitPrice float64 + TotalPrice float64 + UnitCost float64 + TotalCost float64 + RefundAmount float64 + RefundQuantity int + IsPartiallyRefunded bool + IsFullyRefunded bool + RefundReason *string + RefundedAt *time.Time + RefundedBy *uuid.UUID + Modifiers []map[string]interface{} + Notes *string + Metadata map[string]interface{} + Status constants.OrderItemStatus + CreatedAt time.Time + UpdatedAt time.Time +} + +type PaymentOrderItemResponse struct { + ID uuid.UUID + PaymentID uuid.UUID + OrderItemID uuid.UUID + Amount float64 + CreatedAt time.Time + UpdatedAt time.Time +} + +type ListOrdersRequest struct { + OrganizationID *uuid.UUID + OutletID *uuid.UUID + UserID *uuid.UUID + CustomerID *uuid.UUID + OrderType *constants.OrderType + Status *constants.OrderStatus + PaymentStatus *constants.PaymentStatus + IsVoid *bool + IsRefund *bool + DateFrom *time.Time + DateTo *time.Time + Search string + Page int `validate:"required,min=1"` + Limit int `validate:"required,min=1,max=100"` +} + +type ListOrdersResponse struct { + Orders []OrderResponse + TotalCount int + Page int + Limit int + TotalPages int +} + +func (o *Order) CanBeModified() bool { + return o.Status == constants.OrderStatusPending || o.Status == constants.OrderStatusPreparing +} + +func (o *Order) CanBeCancelled() bool { + return o.Status == constants.OrderStatusPending || o.Status == constants.OrderStatusPreparing +} + +func (o *Order) CalculateTotalAmount(taxRate float64) float64 { + taxAmount := o.Subtotal * taxRate + return o.Subtotal + taxAmount - o.DiscountAmount +} + +func (o *Order) IsComplete() bool { + return o.Status == constants.OrderStatusCompleted +} + +func (o *Order) IsPaid() bool { + return o.Status == constants.OrderStatusPaid +} + +func (r *CreateOrderRequest) Validate() bool { + return r.OrderType.IsValidOrderType() +} + +// AddToOrderRequest represents the request to add items to an existing order +type AddToOrderRequest struct { + OrderItems []CreateOrderItemRequest `validate:"required,min=1,dive"` + Notes *string `validate:"omitempty,max=1000"` + Metadata map[string]interface{} `validate:"omitempty"` +} + +// AddToOrderResponse represents the response when adding items to an existing order +type AddToOrderResponse struct { + OrderID uuid.UUID + OrderNumber string + AddedItems []OrderItemResponse + UpdatedOrder OrderResponse +} + +// SetOrderCustomerRequest represents the request to set customer for an order +type SetOrderCustomerRequest struct { + CustomerID uuid.UUID `validate:"required"` +} + +// SetOrderCustomerResponse represents the response when setting customer for an order +type SetOrderCustomerResponse struct { + OrderID uuid.UUID + CustomerID uuid.UUID + Message string +} diff --git a/internal/models/organization.go b/internal/models/organization.go new file mode 100644 index 0000000..8226b63 --- /dev/null +++ b/internal/models/organization.go @@ -0,0 +1,76 @@ +package models + +import ( + "apskel-pos-be/internal/constants" + "time" + + "github.com/google/uuid" +) + +type Organization struct { + ID uuid.UUID + Name string + Email *string + PhoneNumber *string + PlanType constants.PlanType + CreatedAt time.Time + UpdatedAt time.Time +} + +// CreateOrganizationRequest contains both organization and admin user attributes +type CreateOrganizationRequest struct { + // Organization attributes + OrganizationName string `validate:"required,min=1,max=255"` + OrganizationEmail *string `validate:"omitempty,email"` + OrganizationPhoneNumber *string `validate:"omitempty"` + PlanType constants.PlanType `validate:"required"` + + // Admin user attributes + AdminName string `validate:"required,min=1,max=255"` + AdminEmail string `validate:"required,email"` + AdminPassword string `validate:"required,min=6"` + + // Default outlet attributes + OutletName string `validate:"required,min=1,max=255"` + OutletAddress *string + OutletTimezone *string + OutletCurrency string `validate:"required,len=3"` +} + +type UpdateOrganizationRequest struct { + Name *string `validate:"omitempty,min=1,max=255"` + Email *string `validate:"omitempty,email"` + PhoneNumber *string `validate:"omitempty"` + PlanType *constants.PlanType +} + +type OrganizationResponse struct { + ID uuid.UUID + Name string + Email *string + PhoneNumber *string + PlanType constants.PlanType + CreatedAt time.Time + UpdatedAt time.Time +} + +// CreateOrganizationResponse contains the created organization, admin user, and default outlet +type CreateOrganizationResponse struct { + Organization *OrganizationResponse `json:"organization"` + AdminUser *UserResponse `json:"admin_user"` + DefaultOutlet *OutletResponse `json:"default_outlet"` +} + +// Outlet response for the creation response +type OutletResponse struct { + ID uuid.UUID `json:"id"` + OrganizationID uuid.UUID `json:"organization_id"` + Name string `json:"name"` + Address *string `json:"address"` + Timezone *string `json:"timezone"` + Currency string `json:"currency"` + TaxRate float64 `json:"tax_rate"` + IsActive bool `json:"is_active"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} diff --git a/internal/models/outlet.go b/internal/models/outlet.go new file mode 100644 index 0000000..529effb --- /dev/null +++ b/internal/models/outlet.go @@ -0,0 +1,41 @@ +package models + +import ( + "apskel-pos-be/internal/constants" + "time" + + "github.com/google/uuid" +) + +type Outlet struct { + ID uuid.UUID + OrganizationID uuid.UUID + Name string + Address string + PhoneNumber *string + BusinessType constants.BusinessType + Currency constants.Currency + TaxRate float64 + IsActive bool + CreatedAt time.Time + UpdatedAt time.Time +} + +type CreateOutletRequest struct { + OrganizationID uuid.UUID `validate:"required"` + Name string `validate:"required,min=1,max=255"` + Address string `validate:"required,min=1,max=500"` + PhoneNumber *string `validate:"omitempty,e164"` + BusinessType constants.BusinessType `validate:"required"` + Currency constants.Currency `validate:"required"` + TaxRate float64 `validate:"min=0,max=1"` +} + +type UpdateOutletRequest struct { + OrganizationID uuid.UUID + Name *string `validate:"omitempty,min=1,max=255"` + Address *string `validate:"omitempty,min=1,max=500"` + PhoneNumber *string `validate:"omitempty,e164"` + TaxRate *float64 `validate:"omitempty,min=0,max=1"` + IsActive *bool +} diff --git a/internal/models/outlet_setting.go b/internal/models/outlet_setting.go new file mode 100644 index 0000000..cf9f4c0 --- /dev/null +++ b/internal/models/outlet_setting.go @@ -0,0 +1,53 @@ +package models + +import ( + "time" + + "github.com/google/uuid" +) + +type OutletSetting struct { + ID uuid.UUID + OutletID uuid.UUID + Key string + Value string + CreatedAt time.Time + UpdatedAt time.Time +} + +type CreateOutletSettingRequest struct { + OutletID uuid.UUID `validate:"required"` + Key string `validate:"required,min=1,max=255"` + Value string `validate:"required"` +} + +type UpdateOutletSettingRequest struct { + Value string `validate:"required"` +} + +type OutletSettingResponse struct { + ID uuid.UUID `json:"id"` + OutletID uuid.UUID `json:"outlet_id"` + Key string `json:"key"` + Value string `json:"value"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type OutletPrinterSettings struct { + OutletName string `json:"outlet_name"` + Address string `json:"address"` + PhoneNumber string `json:"phone_number"` + PaperSize string `json:"paper_size"` + Footer string `json:"footer"` + FooterHashtag string `json:"footer_hashtag"` +} + +type UpdateOutletPrinterSettingsRequest struct { + OutletName *string `json:"outlet_name,omitempty" validate:"omitempty,min=1,max=255"` + Address *string `json:"address,omitempty" validate:"omitempty,min=1,max=500"` + PhoneNumber *string `json:"phone_number,omitempty" validate:"omitempty,e164"` + PaperSize *string `json:"paper_size,omitempty" validate:"omitempty,oneof=58mm 80mm A4 A5 Letter"` + Footer *string `json:"footer,omitempty" validate:"omitempty,min=1,max=500"` + FooterHashtag *string `json:"footer_hashtag,omitempty" validate:"omitempty,min=1,max=100"` +} diff --git a/internal/models/payment.go b/internal/models/payment.go new file mode 100644 index 0000000..0e1b003 --- /dev/null +++ b/internal/models/payment.go @@ -0,0 +1,81 @@ +package models + +import ( + "apskel-pos-be/internal/constants" + "time" + + "github.com/google/uuid" +) + +type Payment struct { + ID uuid.UUID + OrderID uuid.UUID + PaymentMethodID uuid.UUID + Amount float64 + Status constants.PaymentTransactionStatus + TransactionID *string + SplitNumber int + SplitTotal int + SplitDescription *string + RefundAmount float64 + RefundReason *string + RefundedAt *time.Time + RefundedBy *uuid.UUID + Metadata map[string]interface{} + CreatedAt time.Time + UpdatedAt time.Time +} + +type CreatePaymentRequest struct { + OrderID uuid.UUID `validate:"required"` + PaymentMethodID uuid.UUID `validate:"required"` + Amount float64 `validate:"required,min=0"` + TransactionID *string `validate:"omitempty"` + SplitNumber int `validate:"omitempty,min=1"` + SplitTotal int `validate:"omitempty,min=1"` + SplitDescription *string `validate:"omitempty,max=255"` + PaymentOrderItems []CreatePaymentOrderItemRequest `validate:"omitempty,dive"` + Metadata map[string]interface{} +} + +type UpdatePaymentRequest struct { + Status *constants.PaymentTransactionStatus + TransactionID *string + Notes *string `validate:"omitempty,max=1000"` +} + +type PaymentResponse struct { + ID uuid.UUID + OrderID uuid.UUID + PaymentMethodID uuid.UUID + Amount float64 + Status constants.PaymentTransactionStatus + TransactionID *string + SplitNumber int + SplitTotal int + SplitDescription *string + RefundAmount float64 + RefundReason *string + RefundedAt *time.Time + RefundedBy *uuid.UUID + Metadata map[string]interface{} + CreatedAt time.Time + UpdatedAt time.Time + PaymentOrderItems []PaymentOrderItemResponse +} + +func (p *Payment) IsSuccessful() bool { + return p.Status == constants.PaymentTransactionStatusCompleted +} + +func (p *Payment) IsPending() bool { + return p.Status == constants.PaymentTransactionStatusPending +} + +func (p *Payment) CanBeRefunded() bool { + return p.Status == constants.PaymentTransactionStatusCompleted +} + +func (p *Payment) IsProcessed() bool { + return p.Status == constants.PaymentTransactionStatusCompleted || p.Status == constants.PaymentTransactionStatusFailed +} diff --git a/internal/models/payment_method.go b/internal/models/payment_method.go new file mode 100644 index 0000000..519d288 --- /dev/null +++ b/internal/models/payment_method.go @@ -0,0 +1,77 @@ +package models + +import ( + "apskel-pos-be/internal/constants" + "time" + + "github.com/google/uuid" +) + +type PaymentMethod struct { + ID uuid.UUID + OrganizationID uuid.UUID + Name string + Type constants.PaymentMethodType + Processor *string + Configuration map[string]interface{} + IsActive bool + CreatedAt time.Time + UpdatedAt time.Time +} + +type CreatePaymentMethodRequest struct { + OrganizationID uuid.UUID `validate:"required"` + Name string `validate:"required,min=1,max=100"` + Type constants.PaymentMethodType `validate:"required"` + Processor *string `validate:"omitempty,max=100"` + Configuration map[string]interface{} `validate:"omitempty"` + IsActive *bool `validate:"omitempty"` +} + +type UpdatePaymentMethodRequest struct { + Name *string `validate:"omitempty,min=1,max=100"` + Type *constants.PaymentMethodType `validate:"omitempty"` + Processor *string `validate:"omitempty,max=100"` + Configuration map[string]interface{} `validate:"omitempty"` + IsActive *bool `validate:"omitempty"` +} + +type PaymentMethodResponse struct { + ID uuid.UUID + OrganizationID uuid.UUID + Name string + Type constants.PaymentMethodType + Processor *string + Configuration map[string]interface{} + IsActive bool + CreatedAt time.Time + UpdatedAt time.Time +} + +type ListPaymentMethodsRequest struct { + OrganizationID *uuid.UUID + Type *constants.PaymentMethodType + IsActive *bool + Search string + Page int `validate:"min=1"` + Limit int `validate:"min=1,max=100"` +} + +type ListPaymentMethodsResponse struct { + PaymentMethods []PaymentMethodResponse + TotalCount int + Page int + Limit int + TotalPages int +} + +func (pm *PaymentMethod) IsDigital() bool { + return pm.Type == constants.PaymentMethodTypeCard || + pm.Type == constants.PaymentMethodTypeDigitalWallet || + pm.Type == constants.PaymentMethodTypeEDC || + pm.Type == constants.PaymentMethodTypeQR +} + +func (pm *PaymentMethod) RequiresProcessor() bool { + return pm.IsDigital() && pm.Processor != nil +} diff --git a/internal/models/product.go b/internal/models/product.go new file mode 100644 index 0000000..aa1891c --- /dev/null +++ b/internal/models/product.go @@ -0,0 +1,126 @@ +package models + +import ( + "apskel-pos-be/internal/constants" + "time" + + "github.com/google/uuid" +) + +type Product struct { + ID uuid.UUID + OrganizationID uuid.UUID + CategoryID uuid.UUID + SKU *string + Name string + Description *string + Price float64 + Cost float64 + BusinessType constants.BusinessType + Metadata map[string]interface{} + IsActive bool + CreatedAt time.Time + UpdatedAt time.Time +} + +type ProductVariant struct { + ID uuid.UUID + ProductID uuid.UUID + Name string + PriceModifier float64 + Cost float64 + Metadata map[string]interface{} + CreatedAt time.Time + UpdatedAt time.Time +} + +type CreateProductRequest struct { + OrganizationID uuid.UUID `validate:"required"` + CategoryID uuid.UUID `validate:"required"` + SKU *string `validate:"omitempty,max=100"` + Name string `validate:"required,min=1,max=255"` + Description *string `validate:"omitempty,max=1000"` + Price float64 `validate:"required,min=0"` + Cost float64 `validate:"min=0"` + BusinessType constants.BusinessType `validate:"required"` + Metadata map[string]interface{} + Variants []CreateProductVariantRequest `validate:"omitempty,dive"` + // Stock management fields + InitialStock *int `validate:"omitempty,min=0"` // Initial stock quantity for all outlets + ReorderLevel *int `validate:"omitempty,min=0"` // Reorder level for all outlets + CreateInventory bool `validate:"omitempty"` // Whether to create inventory records for all outlets +} + +type UpdateProductRequest struct { + CategoryID *uuid.UUID + SKU *string `validate:"omitempty,max=100"` + Name *string `validate:"omitempty,min=1,max=255"` + Description *string `validate:"omitempty,max=1000"` + Price *float64 `validate:"omitempty,min=0"` + Cost *float64 `validate:"omitempty,min=0"` + Metadata map[string]interface{} + IsActive *bool + // Stock management fields + ReorderLevel *int `validate:"omitempty,min=0"` // Update reorder level for all existing inventory records +} + +type CreateProductVariantRequest struct { + ProductID uuid.UUID `validate:"required"` + Name string `validate:"required,min=1,max=255"` + PriceModifier float64 `validate:"required"` + Cost float64 `validate:"min=0"` + Metadata map[string]interface{} +} + +type UpdateProductVariantRequest struct { + Name *string `validate:"omitempty,min=1,max=255"` + PriceModifier *float64 + Cost *float64 `validate:"omitempty,min=0"` + Metadata map[string]interface{} +} + +type ProductResponse struct { + ID uuid.UUID + OrganizationID uuid.UUID + CategoryID uuid.UUID + SKU *string + Name string + Description *string + Price float64 + Cost float64 + BusinessType constants.BusinessType + Metadata map[string]interface{} + IsActive bool + CreatedAt time.Time + UpdatedAt time.Time + Variants []ProductVariantResponse +} + +type ProductVariantResponse struct { + ID uuid.UUID + ProductID uuid.UUID + Name string + PriceModifier float64 + Cost float64 + Metadata map[string]interface{} + CreatedAt time.Time + UpdatedAt time.Time +} + +func (p *Product) GetFinalPrice(variant *ProductVariant) float64 { + if variant == nil { + return p.Price + } + return p.Price + variant.PriceModifier +} + +func (p *Product) GetProfitMargin() float64 { + if p.Price == 0 { + return 0 + } + return ((p.Price - p.Cost) / p.Price) * 100 +} + +func (p *Product) IsValidPrice() bool { + return p.Price > 0 && p.Cost >= 0 && p.Price > p.Cost +} diff --git a/internal/models/user.go b/internal/models/user.go new file mode 100644 index 0000000..33fbffb --- /dev/null +++ b/internal/models/user.go @@ -0,0 +1,88 @@ +package models + +import ( + "apskel-pos-be/internal/constants" + "time" + + "github.com/google/uuid" +) + +type User struct { + ID uuid.UUID + OrganizationID uuid.UUID + OutletID *uuid.UUID + Name string + Email string + Role constants.UserRole + Permissions map[string]interface{} + IsActive bool + CreatedAt time.Time + UpdatedAt time.Time +} + +type CreateUserRequest struct { + OrganizationID uuid.UUID `validate:"required"` + OutletID *uuid.UUID + Name string `validate:"required,min=1,max=255"` + Email string `validate:"required,email"` + Password string `validate:"required,min=6"` + Role constants.UserRole `validate:"required"` + Permissions map[string]interface{} `validate:"omitempty"` +} + +type UpdateUserRequest struct { + Name *string `validate:"omitempty,min=1,max=255"` + Email *string `validate:"omitempty,email"` + Role *constants.UserRole + OutletID *uuid.UUID + IsActive *bool + Permissions *map[string]interface{} +} + +type ChangePasswordRequest struct { + CurrentPassword string `validate:"required"` + NewPassword string `validate:"required,min=6"` +} + +type UserResponse struct { + ID uuid.UUID + OrganizationID uuid.UUID + OutletID *uuid.UUID + Name string + Email string + Role constants.UserRole + Permissions map[string]interface{} + IsActive bool + CreatedAt time.Time + UpdatedAt time.Time +} + +func (u *User) HasPermission(requiredRole constants.UserRole) bool { + roleHierarchy := map[constants.UserRole]int{ + constants.RoleWaiter: 1, + constants.RoleCashier: 2, + constants.RoleManager: 3, + constants.RoleAdmin: 4, + } + + userLevel := roleHierarchy[u.Role] + requiredLevel := roleHierarchy[requiredRole] + + return userLevel >= requiredLevel +} + +func (u *User) CanAccessOutlet(outletID uuid.UUID) bool { + if u.Role == constants.RoleAdmin { + return true + } + + return u.OutletID != nil && *u.OutletID == outletID +} + +func (u *User) IsManager() bool { + return u.HasPermission(constants.RoleManager) +} + +func (u *User) IsAdmin() bool { + return u.Role == constants.RoleAdmin +} diff --git a/internal/processor/analytics_processor.go b/internal/processor/analytics_processor.go new file mode 100644 index 0000000..5d88ddb --- /dev/null +++ b/internal/processor/analytics_processor.go @@ -0,0 +1,347 @@ +package processor + +import ( + "context" + "fmt" + "time" + + "apskel-pos-be/internal/models" + "apskel-pos-be/internal/repository" +) + +type AnalyticsProcessor interface { + GetPaymentMethodAnalytics(ctx context.Context, req *models.PaymentMethodAnalyticsRequest) (*models.PaymentMethodAnalyticsResponse, error) + GetSalesAnalytics(ctx context.Context, req *models.SalesAnalyticsRequest) (*models.SalesAnalyticsResponse, error) + GetProductAnalytics(ctx context.Context, req *models.ProductAnalyticsRequest) (*models.ProductAnalyticsResponse, error) + GetDashboardAnalytics(ctx context.Context, req *models.DashboardAnalyticsRequest) (*models.DashboardAnalyticsResponse, error) + GetProfitLossAnalytics(ctx context.Context, req *models.ProfitLossAnalyticsRequest) (*models.ProfitLossAnalyticsResponse, error) +} + +type AnalyticsProcessorImpl struct { + analyticsRepo repository.AnalyticsRepository +} + +func NewAnalyticsProcessorImpl(analyticsRepo repository.AnalyticsRepository) *AnalyticsProcessorImpl { + return &AnalyticsProcessorImpl{ + analyticsRepo: analyticsRepo, + } +} + +func (p *AnalyticsProcessorImpl) GetPaymentMethodAnalytics(ctx context.Context, req *models.PaymentMethodAnalyticsRequest) (*models.PaymentMethodAnalyticsResponse, error) { + if req.DateFrom.After(req.DateTo) { + return nil, fmt.Errorf("date_from cannot be after date_to") + } + + analyticsData, err := p.analyticsRepo.GetPaymentMethodAnalytics(ctx, req.OrganizationID, req.OutletID, req.DateFrom, req.DateTo) + if err != nil { + return nil, fmt.Errorf("failed to get payment method analytics: %w", err) + } + + // Calculate summary + var totalAmount float64 + var totalOrders int64 + var totalPayments int64 + + for _, data := range analyticsData { + totalAmount += data.TotalAmount + totalOrders += data.OrderCount + totalPayments += data.PaymentCount + } + + var averageOrderValue float64 + if totalOrders > 0 { + averageOrderValue = totalAmount / float64(totalOrders) + } + + // Calculate percentages + var resultData []models.PaymentMethodAnalyticsData + for _, data := range analyticsData { + var percentage float64 + if totalAmount > 0 { + percentage = (data.TotalAmount / totalAmount) * 100 + } + + resultData = append(resultData, models.PaymentMethodAnalyticsData{ + PaymentMethodID: data.PaymentMethodID, + PaymentMethodName: data.PaymentMethodName, + PaymentMethodType: data.PaymentMethodType, + TotalAmount: data.TotalAmount, + OrderCount: data.OrderCount, + PaymentCount: data.PaymentCount, + Percentage: percentage, + }) + } + + summary := models.PaymentMethodSummary{ + TotalAmount: totalAmount, + TotalOrders: totalOrders, + TotalPayments: totalPayments, + AverageOrderValue: averageOrderValue, + } + + return &models.PaymentMethodAnalyticsResponse{ + OrganizationID: req.OrganizationID, + OutletID: req.OutletID, + DateFrom: req.DateFrom, + DateTo: req.DateTo, + GroupBy: req.GroupBy, + Summary: summary, + Data: resultData, + }, nil +} + +func (p *AnalyticsProcessorImpl) GetSalesAnalytics(ctx context.Context, req *models.SalesAnalyticsRequest) (*models.SalesAnalyticsResponse, error) { + // Validate date range + if req.DateFrom.After(req.DateTo) { + return nil, fmt.Errorf("date_from cannot be after date_to") + } + + // Validate groupBy + if req.GroupBy == "" { + req.GroupBy = "day" + } + + // Get analytics data from repository + analyticsData, err := p.analyticsRepo.GetSalesAnalytics(ctx, req.OrganizationID, req.OutletID, req.DateFrom, req.DateTo, req.GroupBy) + if err != nil { + return nil, fmt.Errorf("failed to get sales analytics: %w", err) + } + + // Calculate summary + var totalSales float64 + var totalOrders int64 + var totalItems int64 + var totalTax float64 + var totalDiscount float64 + var netSales float64 + + for _, data := range analyticsData { + totalSales += data.Sales + totalOrders += data.Orders + totalItems += data.Items + totalTax += data.Tax + totalDiscount += data.Discount + netSales += data.NetSales + } + + var averageOrderValue float64 + if totalOrders > 0 { + averageOrderValue = totalSales / float64(totalOrders) + } + + // Transform data + var resultData []models.SalesAnalyticsData + for _, data := range analyticsData { + resultData = append(resultData, models.SalesAnalyticsData{ + Date: data.Date, + Sales: data.Sales, + Orders: data.Orders, + Items: data.Items, + Tax: data.Tax, + Discount: data.Discount, + NetSales: data.NetSales, + }) + } + + summary := models.SalesSummary{ + TotalSales: totalSales, + TotalOrders: totalOrders, + TotalItems: totalItems, + AverageOrderValue: averageOrderValue, + TotalTax: totalTax, + TotalDiscount: totalDiscount, + NetSales: netSales, + } + + return &models.SalesAnalyticsResponse{ + OrganizationID: req.OrganizationID, + OutletID: req.OutletID, + DateFrom: req.DateFrom, + DateTo: req.DateTo, + GroupBy: req.GroupBy, + Summary: summary, + Data: resultData, + }, nil +} + +func (p *AnalyticsProcessorImpl) GetProductAnalytics(ctx context.Context, req *models.ProductAnalyticsRequest) (*models.ProductAnalyticsResponse, error) { + // Validate date range + if req.DateFrom.After(req.DateTo) { + return nil, fmt.Errorf("date_from cannot be after date_to") + } + + // Set default limit + if req.Limit <= 0 { + req.Limit = 10 + } + + // Get analytics data from repository + analyticsData, err := p.analyticsRepo.GetProductAnalytics(ctx, req.OrganizationID, req.OutletID, req.DateFrom, req.DateTo, req.Limit) + if err != nil { + return nil, fmt.Errorf("failed to get product analytics: %w", err) + } + + // Transform data + var resultData []models.ProductAnalyticsData + for _, data := range analyticsData { + resultData = append(resultData, models.ProductAnalyticsData{ + ProductID: data.ProductID, + ProductName: data.ProductName, + CategoryID: data.CategoryID, + CategoryName: data.CategoryName, + QuantitySold: data.QuantitySold, + Revenue: data.Revenue, + AveragePrice: data.AveragePrice, + OrderCount: data.OrderCount, + }) + } + + return &models.ProductAnalyticsResponse{ + OrganizationID: req.OrganizationID, + OutletID: req.OutletID, + DateFrom: req.DateFrom, + DateTo: req.DateTo, + Data: resultData, + }, nil +} + +func (p *AnalyticsProcessorImpl) GetDashboardAnalytics(ctx context.Context, req *models.DashboardAnalyticsRequest) (*models.DashboardAnalyticsResponse, error) { + // Validate date range + if req.DateFrom.After(req.DateTo) { + return nil, fmt.Errorf("date_from cannot be after date_to") + } + + // Get dashboard overview + overview, err := p.analyticsRepo.GetDashboardOverview(ctx, req.OrganizationID, req.OutletID, req.DateFrom, req.DateTo) + if err != nil { + return nil, fmt.Errorf("failed to get dashboard overview: %w", err) + } + + // Get top products (limit to 5 for dashboard) + productReq := &models.ProductAnalyticsRequest{ + OrganizationID: req.OrganizationID, + OutletID: req.OutletID, + DateFrom: req.DateFrom, + DateTo: req.DateTo, + Limit: 5, + } + topProducts, err := p.GetProductAnalytics(ctx, productReq) + if err != nil { + return nil, fmt.Errorf("failed to get top products: %w", err) + } + + // Get payment methods + paymentReq := &models.PaymentMethodAnalyticsRequest{ + OrganizationID: req.OrganizationID, + OutletID: req.OutletID, + DateFrom: req.DateFrom, + DateTo: req.DateTo, + GroupBy: "day", + } + paymentMethods, err := p.GetPaymentMethodAnalytics(ctx, paymentReq) + if err != nil { + return nil, fmt.Errorf("failed to get payment methods: %w", err) + } + + // Get recent sales (last 7 days) + recentDateFrom := time.Now().AddDate(0, 0, -7) + salesReq := &models.SalesAnalyticsRequest{ + OrganizationID: req.OrganizationID, + OutletID: req.OutletID, + DateFrom: recentDateFrom, + DateTo: req.DateTo, + GroupBy: "day", + } + recentSales, err := p.GetSalesAnalytics(ctx, salesReq) + if err != nil { + return nil, fmt.Errorf("failed to get recent sales: %w", err) + } + + return &models.DashboardAnalyticsResponse{ + OrganizationID: req.OrganizationID, + OutletID: req.OutletID, + DateFrom: req.DateFrom, + DateTo: req.DateTo, + Overview: models.DashboardOverview{ + TotalSales: overview.TotalSales, + TotalOrders: overview.TotalOrders, + AverageOrderValue: overview.AverageOrderValue, + TotalCustomers: overview.TotalCustomers, + VoidedOrders: overview.VoidedOrders, + RefundedOrders: overview.RefundedOrders, + }, + TopProducts: topProducts.Data, + PaymentMethods: paymentMethods.Data, + RecentSales: recentSales.Data, + }, nil +} + +func (p *AnalyticsProcessorImpl) GetProfitLossAnalytics(ctx context.Context, req *models.ProfitLossAnalyticsRequest) (*models.ProfitLossAnalyticsResponse, error) { + if req.DateFrom.After(req.DateTo) { + return nil, fmt.Errorf("date_from cannot be after date_to") + } + + // Get analytics data from repository + result, err := p.analyticsRepo.GetProfitLossAnalytics(ctx, req.OrganizationID, req.OutletID, req.DateFrom, req.DateTo, req.GroupBy) + if err != nil { + return nil, fmt.Errorf("failed to get profit/loss analytics: %w", err) + } + + // Transform entities to models + data := make([]models.ProfitLossData, len(result.Data)) + for i, item := range result.Data { + data[i] = models.ProfitLossData{ + Date: item.Date, + Revenue: item.Revenue, + Cost: item.Cost, + GrossProfit: item.GrossProfit, + GrossProfitMargin: item.GrossProfitMargin, + Tax: item.Tax, + Discount: item.Discount, + NetProfit: item.NetProfit, + NetProfitMargin: item.NetProfitMargin, + Orders: item.Orders, + } + } + + productData := make([]models.ProductProfitData, len(result.ProductData)) + for i, item := range result.ProductData { + productData[i] = models.ProductProfitData{ + ProductID: item.ProductID, + ProductName: item.ProductName, + CategoryID: item.CategoryID, + CategoryName: item.CategoryName, + QuantitySold: item.QuantitySold, + Revenue: item.Revenue, + Cost: item.Cost, + GrossProfit: item.GrossProfit, + GrossProfitMargin: item.GrossProfitMargin, + AveragePrice: item.AveragePrice, + AverageCost: item.AverageCost, + ProfitPerUnit: item.ProfitPerUnit, + } + } + + return &models.ProfitLossAnalyticsResponse{ + OrganizationID: req.OrganizationID, + OutletID: req.OutletID, + DateFrom: req.DateFrom, + DateTo: req.DateTo, + GroupBy: req.GroupBy, + Summary: models.ProfitLossSummary{ + TotalRevenue: result.Summary.TotalRevenue, + TotalCost: result.Summary.TotalCost, + GrossProfit: result.Summary.GrossProfit, + GrossProfitMargin: result.Summary.GrossProfitMargin, + TotalTax: result.Summary.TotalTax, + TotalDiscount: result.Summary.TotalDiscount, + NetProfit: result.Summary.NetProfit, + NetProfitMargin: result.Summary.NetProfitMargin, + TotalOrders: result.Summary.TotalOrders, + AverageProfit: result.Summary.AverageProfit, + ProfitabilityRatio: result.Summary.ProfitabilityRatio, + }, + Data: data, + ProductData: productData, + }, nil +} diff --git a/internal/processor/category_processor.go b/internal/processor/category_processor.go new file mode 100644 index 0000000..76f68e6 --- /dev/null +++ b/internal/processor/category_processor.go @@ -0,0 +1,152 @@ +package processor + +import ( + "context" + "fmt" + + "apskel-pos-be/internal/entities" + "apskel-pos-be/internal/mappers" + "apskel-pos-be/internal/models" + + "github.com/google/uuid" +) + +type CategoryProcessor interface { + CreateCategory(ctx context.Context, req *models.CreateCategoryRequest) (*models.CategoryResponse, error) + UpdateCategory(ctx context.Context, id uuid.UUID, req *models.UpdateCategoryRequest) (*models.CategoryResponse, error) + DeleteCategory(ctx context.Context, id uuid.UUID) error + GetCategoryByID(ctx context.Context, id uuid.UUID) (*models.CategoryResponse, error) + ListCategories(ctx context.Context, filters map[string]interface{}, page, limit int) ([]models.CategoryResponse, int, error) +} + +type CategoryRepository interface { + Create(ctx context.Context, category *entities.Category) error + GetByID(ctx context.Context, id uuid.UUID) (*entities.Category, error) + GetWithProducts(ctx context.Context, id uuid.UUID) (*entities.Category, error) + GetByOrganization(ctx context.Context, organizationID uuid.UUID) ([]*entities.Category, error) + GetByBusinessType(ctx context.Context, businessType string) ([]*entities.Category, error) + Update(ctx context.Context, category *entities.Category) error + Delete(ctx context.Context, id uuid.UUID) error + List(ctx context.Context, filters map[string]interface{}, limit, offset int) ([]*entities.Category, int64, error) + Count(ctx context.Context, filters map[string]interface{}) (int64, error) + GetByName(ctx context.Context, organizationID uuid.UUID, name string) (*entities.Category, error) + ExistsByName(ctx context.Context, organizationID uuid.UUID, name string, excludeID *uuid.UUID) (bool, error) +} + +type CategoryProcessorImpl struct { + categoryRepo CategoryRepository +} + +func NewCategoryProcessorImpl(categoryRepo CategoryRepository) *CategoryProcessorImpl { + return &CategoryProcessorImpl{ + categoryRepo: categoryRepo, + } +} + +func (p *CategoryProcessorImpl) CreateCategory(ctx context.Context, req *models.CreateCategoryRequest) (*models.CategoryResponse, error) { + // Check if category with same name exists for this organization + exists, err := p.categoryRepo.ExistsByName(ctx, req.OrganizationID, req.Name, nil) + if err != nil { + return nil, fmt.Errorf("failed to check category name uniqueness: %w", err) + } + if exists { + return nil, fmt.Errorf("category with name '%s' already exists for this organization", req.Name) + } + + // Map request to entity + categoryEntity := mappers.CreateCategoryRequestToEntity(req) + + // Create category + if err := p.categoryRepo.Create(ctx, categoryEntity); err != nil { + return nil, fmt.Errorf("failed to create category: %w", err) + } + + // Map entity to response model + response := mappers.CategoryEntityToResponse(categoryEntity) + return response, nil +} + +func (p *CategoryProcessorImpl) UpdateCategory(ctx context.Context, id uuid.UUID, req *models.UpdateCategoryRequest) (*models.CategoryResponse, error) { + // Get existing category + existingCategory, err := p.categoryRepo.GetByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("category not found: %w", err) + } + + // Check name uniqueness if name is being updated + if req.Name != nil && *req.Name != existingCategory.Name { + exists, err := p.categoryRepo.ExistsByName(ctx, existingCategory.OrganizationID, *req.Name, &id) + if err != nil { + return nil, fmt.Errorf("failed to check category name uniqueness: %w", err) + } + if exists { + return nil, fmt.Errorf("category with name '%s' already exists for this organization", *req.Name) + } + } + + // Apply updates to entity + mappers.UpdateCategoryEntityFromRequest(existingCategory, req) + + // Update category + if err := p.categoryRepo.Update(ctx, existingCategory); err != nil { + return nil, fmt.Errorf("failed to update category: %w", err) + } + + // Map entity to response model + response := mappers.CategoryEntityToResponse(existingCategory) + return response, nil +} + +func (p *CategoryProcessorImpl) DeleteCategory(ctx context.Context, id uuid.UUID) error { + // Check if category exists + _, err := p.categoryRepo.GetByID(ctx, id) + if err != nil { + return fmt.Errorf("category not found: %w", err) + } + + // Check if category has products + categoryWithProducts, err := p.categoryRepo.GetWithProducts(ctx, id) + if err != nil { + return fmt.Errorf("failed to check category products: %w", err) + } + + if len(categoryWithProducts.Products) > 0 { + return fmt.Errorf("cannot delete category: it has %d products associated with it", len(categoryWithProducts.Products)) + } + + // Delete category + if err := p.categoryRepo.Delete(ctx, id); err != nil { + return fmt.Errorf("failed to delete category: %w", err) + } + + return nil +} + +func (p *CategoryProcessorImpl) GetCategoryByID(ctx context.Context, id uuid.UUID) (*models.CategoryResponse, error) { + categoryEntity, err := p.categoryRepo.GetByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("category not found: %w", err) + } + + response := mappers.CategoryEntityToResponse(categoryEntity) + return response, nil +} + +func (p *CategoryProcessorImpl) ListCategories(ctx context.Context, filters map[string]interface{}, page, limit int) ([]models.CategoryResponse, int, error) { + offset := (page - 1) * limit + + categoryEntities, total, err := p.categoryRepo.List(ctx, filters, limit, offset) + if err != nil { + return nil, 0, fmt.Errorf("failed to list categories: %w", err) + } + + responses := make([]models.CategoryResponse, len(categoryEntities)) + for i, entity := range categoryEntities { + response := mappers.CategoryEntityToResponse(entity) + if response != nil { + responses[i] = *response + } + } + + return responses, int(total), nil +} diff --git a/internal/processor/customer_processor.go b/internal/processor/customer_processor.go new file mode 100644 index 0000000..608f020 --- /dev/null +++ b/internal/processor/customer_processor.go @@ -0,0 +1,195 @@ +package processor + +import ( + "apskel-pos-be/internal/mappers" + "apskel-pos-be/internal/models" + "apskel-pos-be/internal/repository" + "context" + "errors" + "fmt" + + "github.com/google/uuid" +) + +type CustomerProcessor struct { + customerRepo *repository.CustomerRepository +} + +func NewCustomerProcessor(customerRepo *repository.CustomerRepository) *CustomerProcessor { + return &CustomerProcessor{ + customerRepo: customerRepo, + } +} + +// CreateCustomer creates a new customer +func (p *CustomerProcessor) CreateCustomer(ctx context.Context, req *models.CreateCustomerRequest, organizationID uuid.UUID) (*models.CustomerResponse, error) { + if req.Email != nil { + existingCustomer, err := p.customerRepo.GetByEmail(ctx, *req.Email, organizationID) + if err == nil && existingCustomer != nil { + return nil, errors.New("email already exists for this organization") + } + } + + // Convert request to entity + customer := mappers.ToCustomerEntity(req, organizationID) + + // Create customer + err := p.customerRepo.Create(ctx, customer) + if err != nil { + return nil, fmt.Errorf("failed to create customer: %w", err) + } + + return mappers.ToCustomerResponse(customer), nil +} + +// GetCustomer retrieves a customer by ID +func (p *CustomerProcessor) GetCustomer(ctx context.Context, customerID, organizationID uuid.UUID) (*models.CustomerResponse, error) { + customer, err := p.customerRepo.GetByIDAndOrganization(ctx, customerID, organizationID) + if err != nil { + return nil, fmt.Errorf("customer not found: %w", err) + } + + return mappers.ToCustomerResponse(customer), nil +} + +// ListCustomers retrieves customers with pagination and filtering +func (p *CustomerProcessor) ListCustomers(ctx context.Context, query *models.ListCustomersQuery, organizationID uuid.UUID) (*models.PaginatedResponse[models.CustomerResponse], error) { + // Set default values + if query.Page <= 0 { + query.Page = 1 + } + if query.Limit <= 0 { + query.Limit = 10 + } + if query.Limit > 100 { + query.Limit = 100 + } + + offset := (query.Page - 1) * query.Limit + + // Get customers from repository + customers, total, err := p.customerRepo.List( + ctx, + organizationID, + offset, + query.Limit, + query.Search, + query.IsActive, + query.IsDefault, + query.SortBy, + query.SortOrder, + ) + if err != nil { + return nil, fmt.Errorf("failed to list customers: %w", err) + } + + // Convert to responses + responses := mappers.ToCustomerResponses(customers) + + // Calculate pagination info + totalPages := int((total + int64(query.Limit) - 1) / int64(query.Limit)) + + return &models.PaginatedResponse[models.CustomerResponse]{ + Data: responses, + Pagination: models.Pagination{ + Page: query.Page, + Limit: query.Limit, + Total: total, + TotalPages: totalPages, + }, + }, nil +} + +// UpdateCustomer updates an existing customer +func (p *CustomerProcessor) UpdateCustomer(ctx context.Context, customerID, organizationID uuid.UUID, req *models.UpdateCustomerRequest) (*models.CustomerResponse, error) { + // Get existing customer + customer, err := p.customerRepo.GetByIDAndOrganization(ctx, customerID, organizationID) + if err != nil { + return nil, fmt.Errorf("customer not found: %w", err) + } + + // Check if email is already taken by another customer within the organization + if req.Email != nil && *req.Email != *customer.Email { + existingCustomer, err := p.customerRepo.GetByEmail(ctx, *req.Email, organizationID) + if err == nil && existingCustomer != nil && existingCustomer.ID != customerID { + return nil, errors.New("email already exists for this organization") + } + } + + // Update customer fields + mappers.UpdateCustomerEntity(customer, req) + + // Save updated customer + err = p.customerRepo.Update(ctx, customer) + if err != nil { + return nil, fmt.Errorf("failed to update customer: %w", err) + } + + return mappers.ToCustomerResponse(customer), nil +} + +// DeleteCustomer deletes a customer +func (p *CustomerProcessor) DeleteCustomer(ctx context.Context, customerID, organizationID uuid.UUID) error { + // Get existing customer + customer, err := p.customerRepo.GetByIDAndOrganization(ctx, customerID, organizationID) + if err != nil { + return fmt.Errorf("customer not found: %w", err) + } + + // Prevent deletion of default customer + if customer.IsDefault { + return errors.New("cannot delete default customer") + } + + // Delete customer + err = p.customerRepo.Delete(ctx, customerID) + if err != nil { + return fmt.Errorf("failed to delete customer: %w", err) + } + + return nil +} + +func (p *CustomerProcessor) SetDefaultCustomer(ctx context.Context, customerID, organizationID uuid.UUID) (*models.CustomerResponse, error) { + _, err := p.customerRepo.GetByIDAndOrganization(ctx, customerID, organizationID) + if err != nil { + return nil, fmt.Errorf("customer not found: %w", err) + } + + // Set as default + err = p.customerRepo.SetAsDefault(ctx, customerID, organizationID) + if err != nil { + return nil, fmt.Errorf("failed to set default customer: %w", err) + } + + // Get updated customer + updatedCustomer, err := p.customerRepo.GetByID(ctx, customerID) + if err != nil { + return nil, fmt.Errorf("failed to get updated customer: %w", err) + } + + return mappers.ToCustomerResponse(updatedCustomer), nil +} + +func (p *CustomerProcessor) GetDefaultCustomer(ctx context.Context, organizationID uuid.UUID) (*models.CustomerResponse, error) { + customer, err := p.customerRepo.GetDefaultCustomer(ctx, organizationID) + if err != nil { + return nil, fmt.Errorf("default customer not found: %w", err) + } + + return mappers.ToCustomerResponse(customer), nil +} + +func (p *CustomerProcessor) EnsureDefaultCustomer(ctx context.Context, organizationID uuid.UUID) (*models.CustomerResponse, error) { + customer, err := p.customerRepo.GetDefaultCustomer(ctx, organizationID) + if err == nil { + return mappers.ToCustomerResponse(customer), nil + } + + defaultCustomer, err := p.customerRepo.CreateDefaultCustomer(ctx, organizationID) + if err != nil { + return nil, fmt.Errorf("failed to create default customer: %w", err) + } + + return mappers.ToCustomerResponse(defaultCustomer), nil +} diff --git a/internal/processor/file_manager_client.go b/internal/processor/file_manager_client.go new file mode 100644 index 0000000..8cf50d3 --- /dev/null +++ b/internal/processor/file_manager_client.go @@ -0,0 +1,7 @@ +package processor + +import "context" + +type FileClient interface { + UploadFile(ctx context.Context, fileName string, fileContent []byte) (fileUrl string, err error) +} diff --git a/internal/processor/file_manager_processor.go b/internal/processor/file_manager_processor.go new file mode 100644 index 0000000..c62186e --- /dev/null +++ b/internal/processor/file_manager_processor.go @@ -0,0 +1,227 @@ +package processor + +import ( + "context" + "fmt" + "mime/multipart" + "path/filepath" + "strings" + "time" + + "apskel-pos-be/internal/constants" + "apskel-pos-be/internal/entities" + "apskel-pos-be/internal/mappers" + "apskel-pos-be/internal/models" + + "github.com/google/uuid" +) + +type FileProcessor interface { + UploadFile(ctx context.Context, file *multipart.FileHeader, req *models.UploadFileRequest, organizationID, userID uuid.UUID) (*models.FileResponse, error) + GetFileByID(ctx context.Context, id uuid.UUID) (*models.FileResponse, error) + UpdateFile(ctx context.Context, id uuid.UUID, req *models.UpdateFileRequest) (*models.FileResponse, error) + ListFiles(ctx context.Context, req *models.ListFilesRequest) (*models.ListFilesResponse, error) + GetFileByOrganizationID(ctx context.Context, organizationID uuid.UUID) ([]*models.FileResponse, error) + GetFileByUserID(ctx context.Context, userID uuid.UUID) ([]*models.FileResponse, error) +} + +type FileRepository interface { + Create(ctx context.Context, file *entities.File) error + GetByID(ctx context.Context, id uuid.UUID) (*entities.File, error) + GetByOrganizationID(ctx context.Context, organizationID uuid.UUID) ([]*entities.File, error) + GetByUserID(ctx context.Context, userID uuid.UUID) ([]*entities.File, error) + Update(ctx context.Context, file *entities.File) error + Delete(ctx context.Context, id uuid.UUID) error + List(ctx context.Context, filters map[string]interface{}, limit, offset int) ([]*entities.File, int64, error) + GetByFileName(ctx context.Context, fileName string) (*entities.File, error) + ExistsByFileName(ctx context.Context, fileName string) (bool, error) +} + +type FileProcessorImpl struct { + fileRepo FileRepository + fileClient FileClient +} + +func NewFileProcessorImpl(fileRepo FileRepository, fileClient FileClient) *FileProcessorImpl { + return &FileProcessorImpl{ + fileRepo: fileRepo, + fileClient: fileClient, + } +} + +func (p *FileProcessorImpl) UploadFile(ctx context.Context, file *multipart.FileHeader, req *models.UploadFileRequest, organizationID, userID uuid.UUID) (*models.FileResponse, error) { + if file == nil { + return nil, fmt.Errorf("file is required") + } + + if file.Size == 0 { + return nil, fmt.Errorf("file cannot be empty") + } + + const maxFileSize = 10 * 1024 * 1024 // 10MB + if file.Size > maxFileSize { + return nil, fmt.Errorf("file size exceeds maximum limit of 10MB") + } + + if !constants.IsValidFileType(req.FileType) { + return nil, fmt.Errorf("invalid file type: %s", req.FileType) + } + + originalName := file.Filename + fileName := mappers.GenerateFileName(originalName, organizationID, userID) + + for { + exists, err := p.fileRepo.ExistsByFileName(ctx, fileName) + if err != nil { + return nil, fmt.Errorf("failed to check filename uniqueness: %w", err) + } + if !exists { + break + } + + ext := filepath.Ext(fileName) + base := strings.TrimSuffix(fileName, ext) + fileName = fmt.Sprintf("%s_%d%s", base, time.Now().UnixNano(), ext) + } + + src, err := file.Open() + if err != nil { + return nil, fmt.Errorf("failed to open file: %w", err) + } + defer src.Close() + + fileContent := make([]byte, file.Size) + _, err = src.Read(fileContent) + if err != nil { + return nil, fmt.Errorf("failed to read file content: %w", err) + } + + fileURL, err := p.fileClient.UploadFile(ctx, fileName, fileContent) + if err != nil { + return nil, fmt.Errorf("failed to upload file to storage: %w", err) + } + + mimeType := file.Header.Get("Content-Type") + if mimeType == "" { + mimeType = "application/octet-stream" + } + + fileType := req.FileType + if fileType == "" { + fileType = constants.GetFileTypeFromMimeType(mimeType) + } + + fileEntity := mappers.UploadFileRequestToEntity( + &models.UploadFileRequest{ + FileType: fileType, + IsPublic: req.IsPublic, + Metadata: req.Metadata, + }, + organizationID, + userID, + fileName, + originalName, + fileURL, + mimeType, + fileName, + file.Size, + ) + + if err := p.fileRepo.Create(ctx, fileEntity); err != nil { + return nil, fmt.Errorf("failed to save file metadata: %w", err) + } + + response := mappers.FileEntityToResponse(fileEntity) + return response, nil +} + +func (p *FileProcessorImpl) GetFileByID(ctx context.Context, id uuid.UUID) (*models.FileResponse, error) { + file, err := p.fileRepo.GetByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("failed to get file: %w", err) + } + + response := mappers.FileEntityToResponse(file) + return response, nil +} + +func (p *FileProcessorImpl) UpdateFile(ctx context.Context, id uuid.UUID, req *models.UpdateFileRequest) (*models.FileResponse, error) { + file, err := p.fileRepo.GetByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("failed to get file: %w", err) + } + + updates := mappers.UpdateFileRequestToEntityUpdates(req) + for key, value := range updates { + switch key { + case "is_public": + file.IsPublic = value.(bool) + case "metadata": + file.Metadata = entities.Metadata(value.(map[string]interface{})) + } + } + + if err := p.fileRepo.Update(ctx, file); err != nil { + return nil, fmt.Errorf("failed to update file: %w", err) + } + + response := mappers.FileEntityToResponse(file) + return response, nil +} + +func (p *FileProcessorImpl) ListFiles(ctx context.Context, req *models.ListFilesRequest) (*models.ListFilesResponse, error) { + filters := make(map[string]interface{}) + + if req.OrganizationID != nil { + filters["organization_id"] = *req.OrganizationID + } + if req.UserID != nil { + filters["user_id"] = *req.UserID + } + if req.FileType != nil { + filters["file_type"] = string(*req.FileType) + } + if req.IsPublic != nil { + filters["is_public"] = *req.IsPublic + } + + offset := (req.Page - 1) * req.Limit + + files, total, err := p.fileRepo.List(ctx, filters, req.Limit, offset) + if err != nil { + return nil, fmt.Errorf("failed to list files: %w", err) + } + + fileResponses := mappers.FileEntitiesToResponses(files) + totalPages := (int(total) + req.Limit - 1) / req.Limit + + response := &models.ListFilesResponse{ + Files: fileResponses, + TotalCount: int(total), + Page: req.Page, + Limit: req.Limit, + TotalPages: totalPages, + } + + return response, nil +} + +func (p *FileProcessorImpl) GetFileByOrganizationID(ctx context.Context, organizationID uuid.UUID) ([]*models.FileResponse, error) { + files, err := p.fileRepo.GetByOrganizationID(ctx, organizationID) + if err != nil { + return nil, fmt.Errorf("failed to get files by organization: %w", err) + } + + responses := mappers.FileEntitiesToResponses(files) + return responses, nil +} + +func (p *FileProcessorImpl) GetFileByUserID(ctx context.Context, userID uuid.UUID) ([]*models.FileResponse, error) { + files, err := p.fileRepo.GetByUserID(ctx, userID) + if err != nil { + return nil, fmt.Errorf("failed to get files by user: %w", err) + } + + responses := mappers.FileEntitiesToResponses(files) + return responses, nil +} diff --git a/internal/processor/inventory_processor.go b/internal/processor/inventory_processor.go new file mode 100644 index 0000000..e2fe70e --- /dev/null +++ b/internal/processor/inventory_processor.go @@ -0,0 +1,245 @@ +package processor + +import ( + "context" + "fmt" + + "apskel-pos-be/internal/entities" + "apskel-pos-be/internal/mappers" + "apskel-pos-be/internal/models" + + "github.com/google/uuid" +) + +type InventoryProcessor interface { + CreateInventory(ctx context.Context, req *models.CreateInventoryRequest) (*models.InventoryResponse, error) + UpdateInventory(ctx context.Context, id uuid.UUID, req *models.UpdateInventoryRequest) (*models.InventoryResponse, error) + DeleteInventory(ctx context.Context, id uuid.UUID) error + GetInventoryByID(ctx context.Context, id uuid.UUID) (*models.InventoryResponse, error) + ListInventory(ctx context.Context, filters map[string]interface{}, page, limit int) ([]models.InventoryResponse, int, error) + AdjustInventory(ctx context.Context, productID, outletID uuid.UUID, req *models.InventoryAdjustmentRequest) (*models.InventoryResponse, error) + GetLowStockItems(ctx context.Context, outletID uuid.UUID) ([]models.InventoryResponse, error) + GetZeroStockItems(ctx context.Context, outletID uuid.UUID) ([]models.InventoryResponse, error) +} + +type InventoryRepository interface { + Create(ctx context.Context, inventory *entities.Inventory) error + GetByID(ctx context.Context, id uuid.UUID) (*entities.Inventory, error) + GetWithRelations(ctx context.Context, id uuid.UUID) (*entities.Inventory, error) + GetByProductAndOutlet(ctx context.Context, productID, outletID uuid.UUID) (*entities.Inventory, error) + GetByOutlet(ctx context.Context, outletID uuid.UUID) ([]*entities.Inventory, error) + GetByProduct(ctx context.Context, productID uuid.UUID) ([]*entities.Inventory, error) + GetLowStock(ctx context.Context, outletID uuid.UUID) ([]*entities.Inventory, error) + GetZeroStock(ctx context.Context, outletID uuid.UUID) ([]*entities.Inventory, error) + Update(ctx context.Context, inventory *entities.Inventory) error + Delete(ctx context.Context, id uuid.UUID) error + List(ctx context.Context, filters map[string]interface{}, limit, offset int) ([]*entities.Inventory, int64, error) + Count(ctx context.Context, filters map[string]interface{}) (int64, error) + AdjustQuantity(ctx context.Context, productID, outletID uuid.UUID, delta int) (*entities.Inventory, error) + SetQuantity(ctx context.Context, productID, outletID uuid.UUID, quantity int) (*entities.Inventory, error) + UpdateReorderLevel(ctx context.Context, id uuid.UUID, reorderLevel int) error + BulkCreate(ctx context.Context, inventoryItems []*entities.Inventory) error + BulkAdjustQuantity(ctx context.Context, adjustments map[uuid.UUID]int, outletID uuid.UUID) error + GetTotalValueByOutlet(ctx context.Context, outletID uuid.UUID) (float64, error) +} + +type InventoryProcessorImpl struct { + inventoryRepo InventoryRepository + productRepo ProductRepository + outletRepo OutletRepository +} + +func NewInventoryProcessorImpl( + inventoryRepo InventoryRepository, + productRepo ProductRepository, + outletRepo OutletRepository, +) *InventoryProcessorImpl { + return &InventoryProcessorImpl{ + inventoryRepo: inventoryRepo, + productRepo: productRepo, + outletRepo: outletRepo, + } +} + +func (p *InventoryProcessorImpl) CreateInventory(ctx context.Context, req *models.CreateInventoryRequest) (*models.InventoryResponse, error) { + _, err := p.productRepo.GetByID(ctx, req.ProductID) + if err != nil { + return nil, fmt.Errorf("invalid product: %w", err) + } + + // Validate outlet exists + _, err = p.outletRepo.GetByID(ctx, req.OutletID) + if err != nil { + return nil, fmt.Errorf("invalid outlet: %w", err) + } + + // Check if inventory already exists for this product-outlet combination + existingInventory, err := p.inventoryRepo.GetByProductAndOutlet(ctx, req.ProductID, req.OutletID) + if err == nil && existingInventory != nil { + return nil, fmt.Errorf("inventory already exists for product %s in outlet %s", req.ProductID, req.OutletID) + } + + // Map request to entity + inventoryEntity := mappers.CreateInventoryRequestToEntity(req) + + // Create inventory + if err := p.inventoryRepo.Create(ctx, inventoryEntity); err != nil { + return nil, fmt.Errorf("failed to create inventory: %w", err) + } + + // Get inventory with relations for response + inventoryWithRelations, err := p.inventoryRepo.GetWithRelations(ctx, inventoryEntity.ID) + if err != nil { + return nil, fmt.Errorf("failed to retrieve created inventory: %w", err) + } + + // Map entity to response model + response := mappers.InventoryEntityToResponse(inventoryWithRelations) + return response, nil +} + +func (p *InventoryProcessorImpl) UpdateInventory(ctx context.Context, id uuid.UUID, req *models.UpdateInventoryRequest) (*models.InventoryResponse, error) { + // Get existing inventory + existingInventory, err := p.inventoryRepo.GetByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("inventory not found: %w", err) + } + + // Apply updates to entity + mappers.UpdateInventoryEntityFromRequest(existingInventory, req) + + // Update inventory + if err := p.inventoryRepo.Update(ctx, existingInventory); err != nil { + return nil, fmt.Errorf("failed to update inventory: %w", err) + } + + // Get updated inventory with relations for response + inventoryWithRelations, err := p.inventoryRepo.GetWithRelations(ctx, id) + if err != nil { + return nil, fmt.Errorf("failed to retrieve updated inventory: %w", err) + } + + // Map entity to response model + response := mappers.InventoryEntityToResponse(inventoryWithRelations) + return response, nil +} + +func (p *InventoryProcessorImpl) DeleteInventory(ctx context.Context, id uuid.UUID) error { + // Check if inventory exists + _, err := p.inventoryRepo.GetByID(ctx, id) + if err != nil { + return fmt.Errorf("inventory not found: %w", err) + } + + // Delete inventory + if err := p.inventoryRepo.Delete(ctx, id); err != nil { + return fmt.Errorf("failed to delete inventory: %w", err) + } + + return nil +} + +func (p *InventoryProcessorImpl) GetInventoryByID(ctx context.Context, id uuid.UUID) (*models.InventoryResponse, error) { + inventoryEntity, err := p.inventoryRepo.GetWithRelations(ctx, id) + if err != nil { + return nil, fmt.Errorf("inventory not found: %w", err) + } + + response := mappers.InventoryEntityToResponse(inventoryEntity) + return response, nil +} + +func (p *InventoryProcessorImpl) ListInventory(ctx context.Context, filters map[string]interface{}, page, limit int) ([]models.InventoryResponse, int, error) { + offset := (page - 1) * limit + + inventoryEntities, total, err := p.inventoryRepo.List(ctx, filters, limit, offset) + if err != nil { + return nil, 0, fmt.Errorf("failed to list inventory: %w", err) + } + + responses := make([]models.InventoryResponse, len(inventoryEntities)) + for i, entity := range inventoryEntities { + response := mappers.InventoryEntityToResponse(entity) + if response != nil { + responses[i] = *response + } + } + + return responses, int(total), nil +} + +func (p *InventoryProcessorImpl) AdjustInventory(ctx context.Context, productID, outletID uuid.UUID, req *models.InventoryAdjustmentRequest) (*models.InventoryResponse, error) { + // Validate product exists + _, err := p.productRepo.GetByID(ctx, productID) + if err != nil { + return nil, fmt.Errorf("invalid product: %w", err) + } + + // Validate outlet exists + _, err = p.outletRepo.GetByID(ctx, outletID) + if err != nil { + return nil, fmt.Errorf("invalid outlet: %w", err) + } + + // Perform quantity adjustment + adjustedInventory, err := p.inventoryRepo.AdjustQuantity(ctx, productID, outletID, req.Delta) + if err != nil { + return nil, fmt.Errorf("failed to adjust inventory quantity: %w", err) + } + + // Get inventory with relations for response + inventoryWithRelations, err := p.inventoryRepo.GetWithRelations(ctx, adjustedInventory.ID) + if err != nil { + return nil, fmt.Errorf("failed to retrieve adjusted inventory: %w", err) + } + + // Map entity to response model + response := mappers.InventoryEntityToResponse(inventoryWithRelations) + return response, nil +} + +func (p *InventoryProcessorImpl) GetLowStockItems(ctx context.Context, outletID uuid.UUID) ([]models.InventoryResponse, error) { + // Validate outlet exists + _, err := p.outletRepo.GetByID(ctx, outletID) + if err != nil { + return nil, fmt.Errorf("invalid outlet: %w", err) + } + + inventoryEntities, err := p.inventoryRepo.GetLowStock(ctx, outletID) + if err != nil { + return nil, fmt.Errorf("failed to get low stock items: %w", err) + } + + responses := make([]models.InventoryResponse, len(inventoryEntities)) + for i, entity := range inventoryEntities { + response := mappers.InventoryEntityToResponse(entity) + if response != nil { + responses[i] = *response + } + } + + return responses, nil +} + +func (p *InventoryProcessorImpl) GetZeroStockItems(ctx context.Context, outletID uuid.UUID) ([]models.InventoryResponse, error) { + // Validate outlet exists + _, err := p.outletRepo.GetByID(ctx, outletID) + if err != nil { + return nil, fmt.Errorf("invalid outlet: %w", err) + } + + inventoryEntities, err := p.inventoryRepo.GetZeroStock(ctx, outletID) + if err != nil { + return nil, fmt.Errorf("failed to get zero stock items: %w", err) + } + + responses := make([]models.InventoryResponse, len(inventoryEntities)) + for i, entity := range inventoryEntities { + response := mappers.InventoryEntityToResponse(entity) + if response != nil { + responses[i] = *response + } + } + + return responses, nil +} diff --git a/internal/processor/order_processor.go b/internal/processor/order_processor.go new file mode 100644 index 0000000..3cd11f4 --- /dev/null +++ b/internal/processor/order_processor.go @@ -0,0 +1,884 @@ +package processor + +import ( + "context" + "fmt" + + "apskel-pos-be/internal/constants" + "apskel-pos-be/internal/entities" + "apskel-pos-be/internal/mappers" + "apskel-pos-be/internal/models" + + "github.com/google/uuid" +) + +type OrderProcessor interface { + CreateOrder(ctx context.Context, req *models.CreateOrderRequest, organizationID uuid.UUID) (*models.OrderResponse, error) + AddToOrder(ctx context.Context, orderID uuid.UUID, req *models.AddToOrderRequest) (*models.AddToOrderResponse, error) + UpdateOrder(ctx context.Context, id uuid.UUID, req *models.UpdateOrderRequest) (*models.OrderResponse, error) + GetOrderByID(ctx context.Context, id uuid.UUID) (*models.OrderResponse, error) + ListOrders(ctx context.Context, req *models.ListOrdersRequest) (*models.ListOrdersResponse, error) + VoidOrder(ctx context.Context, req *models.VoidOrderRequest, voidedBy uuid.UUID) error + RefundOrder(ctx context.Context, id uuid.UUID, req *models.RefundOrderRequest, refundedBy uuid.UUID) error + CreatePayment(ctx context.Context, req *models.CreatePaymentRequest) (*models.PaymentResponse, error) + RefundPayment(ctx context.Context, paymentID uuid.UUID, refundAmount float64, reason string, refundedBy uuid.UUID) error + SetOrderCustomer(ctx context.Context, orderID uuid.UUID, req *models.SetOrderCustomerRequest, organizationID uuid.UUID) (*models.SetOrderCustomerResponse, error) +} + +type OrderRepository interface { + Create(ctx context.Context, order *entities.Order) error + GetByID(ctx context.Context, id uuid.UUID) (*entities.Order, error) + GetWithRelations(ctx context.Context, id uuid.UUID) (*entities.Order, error) + Update(ctx context.Context, order *entities.Order) error + Delete(ctx context.Context, id uuid.UUID) error + List(ctx context.Context, filters map[string]interface{}, limit, offset int) ([]*entities.Order, int64, error) + GetByOrderNumber(ctx context.Context, orderNumber string) (*entities.Order, error) + ExistsByOrderNumber(ctx context.Context, orderNumber string) (bool, error) + VoidOrder(ctx context.Context, id uuid.UUID, reason string, voidedBy uuid.UUID) error + VoidOrderWithStatus(ctx context.Context, id uuid.UUID, status entities.OrderStatus, reason string, voidedBy uuid.UUID) error + RefundOrder(ctx context.Context, id uuid.UUID, reason string, refundedBy uuid.UUID) error + UpdatePaymentStatus(ctx context.Context, id uuid.UUID, status entities.PaymentStatus) error + UpdateStatus(ctx context.Context, id uuid.UUID, status entities.OrderStatus) error + GetNextOrderNumber(ctx context.Context, organizationID, outletID uuid.UUID) (string, error) +} + +type OrderItemRepository interface { + Create(ctx context.Context, orderItem *entities.OrderItem) error + GetByID(ctx context.Context, id uuid.UUID) (*entities.OrderItem, error) + GetByOrderID(ctx context.Context, orderID uuid.UUID) ([]*entities.OrderItem, error) + Update(ctx context.Context, orderItem *entities.OrderItem) error + Delete(ctx context.Context, id uuid.UUID) error + RefundOrderItem(ctx context.Context, id uuid.UUID, refundQuantity int, refundAmount float64, reason string, refundedBy uuid.UUID) error + VoidOrderItem(ctx context.Context, id uuid.UUID, voidQuantity int, reason string, voidedBy uuid.UUID) error + UpdateStatus(ctx context.Context, id uuid.UUID, status entities.OrderItemStatus) error +} + +type PaymentRepository interface { + Create(ctx context.Context, payment *entities.Payment) error + GetByID(ctx context.Context, id uuid.UUID) (*entities.Payment, error) + GetByOrderID(ctx context.Context, orderID uuid.UUID) ([]*entities.Payment, error) + Update(ctx context.Context, payment *entities.Payment) error + Delete(ctx context.Context, id uuid.UUID) error + RefundPayment(ctx context.Context, id uuid.UUID, refundAmount float64, reason string, refundedBy uuid.UUID) error + UpdateStatus(ctx context.Context, id uuid.UUID, status entities.PaymentTransactionStatus) error + GetTotalPaidByOrderID(ctx context.Context, orderID uuid.UUID) (float64, error) +} + +type PaymentMethodRepository interface { + GetByID(ctx context.Context, id uuid.UUID) (*entities.PaymentMethod, error) +} + +type ProductVariantRepository interface { + GetByID(ctx context.Context, id uuid.UUID) (*entities.ProductVariant, error) +} + +type CustomerRepository interface { + GetByIDAndOrganization(ctx context.Context, id, organizationID uuid.UUID) (*entities.Customer, error) +} + +type SimplePaymentMethodRepository struct{} + +func (r *SimplePaymentMethodRepository) GetByID(ctx context.Context, id uuid.UUID) (*entities.PaymentMethod, error) { + // TODO: Implement proper payment method repository + // For now, return a mock payment method + return &entities.PaymentMethod{ + ID: id, + Name: "Cash", + Type: entities.PaymentMethodTypeCash, + }, nil +} + +type OrderProcessorImpl struct { + orderRepo OrderRepository + orderItemRepo OrderItemRepository + paymentRepo PaymentRepository + productRepo ProductRepository + paymentMethodRepo PaymentMethodRepository + inventoryRepo InventoryRepository + productVariantRepo ProductVariantRepository + outletRepo OutletRepository + customerRepo CustomerRepository +} + +func NewOrderProcessorImpl( + orderRepo OrderRepository, + orderItemRepo OrderItemRepository, + paymentRepo PaymentRepository, + productRepo ProductRepository, + paymentMethodRepo PaymentMethodRepository, + inventoryRepo InventoryRepository, + productVariantRepo ProductVariantRepository, + outletRepo OutletRepository, + customerRepo CustomerRepository, +) *OrderProcessorImpl { + return &OrderProcessorImpl{ + orderRepo: orderRepo, + orderItemRepo: orderItemRepo, + paymentRepo: paymentRepo, + productRepo: productRepo, + paymentMethodRepo: paymentMethodRepo, + inventoryRepo: inventoryRepo, + productVariantRepo: productVariantRepo, + outletRepo: outletRepo, + customerRepo: customerRepo, + } +} + +func (p *OrderProcessorImpl) CreateOrder(ctx context.Context, req *models.CreateOrderRequest, organizationID uuid.UUID) (*models.OrderResponse, error) { + orderNumber, err := p.orderRepo.GetNextOrderNumber(ctx, organizationID, req.OutletID) + if err != nil { + return nil, fmt.Errorf("failed to generate order number: %w", err) + } + + outlet, err := p.outletRepo.GetByID(ctx, req.OutletID) + if err != nil { + return nil, fmt.Errorf("outlet not found: %w", err) + } + + var subtotal, totalCost float64 + var orderItems []*entities.OrderItem + + for _, itemReq := range req.OrderItems { + product, err := p.productRepo.GetByID(ctx, itemReq.ProductID) + if err != nil { + return nil, fmt.Errorf("product not found: %w", err) + } + + unitPrice := product.Price + unitCost := product.Cost + + if itemReq.ProductVariantID != nil { + variant, err := p.productVariantRepo.GetByID(ctx, *itemReq.ProductVariantID) + if err != nil { + return nil, fmt.Errorf("product variant not found: %w", err) + } + + if variant.ProductID != itemReq.ProductID { + return nil, fmt.Errorf("product variant does not belong to the specified product") + } + + unitPrice += variant.PriceModifier + if variant.Cost > 0 { + unitCost = variant.Cost + } + } + + itemTotalPrice := float64(itemReq.Quantity) * unitPrice + itemTotalCost := float64(itemReq.Quantity) * unitCost + + subtotal += itemTotalPrice + totalCost += itemTotalCost + + orderItem := &entities.OrderItem{ + ProductID: itemReq.ProductID, + ProductVariantID: itemReq.ProductVariantID, + Quantity: itemReq.Quantity, + UnitPrice: unitPrice, // Use price from database + TotalPrice: itemTotalPrice, + UnitCost: unitCost, + TotalCost: itemTotalCost, + Modifiers: entities.Modifiers(itemReq.Modifiers), + Notes: itemReq.Notes, + Metadata: entities.Metadata(itemReq.Metadata), + Status: entities.OrderItemStatusPending, + } + + orderItems = append(orderItems, orderItem) + } + + taxAmount := subtotal * outlet.TaxRate + totalAmount := subtotal + taxAmount + + metadata := entities.Metadata(req.Metadata) + if req.CustomerName != nil { + if metadata == nil { + metadata = make(entities.Metadata) + } + metadata["customer_name"] = *req.CustomerName + } + order := &entities.Order{ + OrganizationID: organizationID, + OutletID: req.OutletID, + UserID: req.UserID, + CustomerID: req.CustomerID, + OrderNumber: orderNumber, + TableNumber: req.TableNumber, + OrderType: entities.OrderType(req.OrderType), + Status: entities.OrderStatusPending, + Subtotal: subtotal, + TaxAmount: taxAmount, + DiscountAmount: 0, + TotalAmount: totalAmount, + TotalCost: totalCost, + PaymentStatus: entities.PaymentStatusPending, + IsVoid: false, + IsRefund: false, + Metadata: metadata, + } + + if err := p.orderRepo.Create(ctx, order); err != nil { + return nil, fmt.Errorf("failed to create order: %w", err) + } + + for _, orderItem := range orderItems { + orderItem.OrderID = order.ID + if err := p.orderItemRepo.Create(ctx, orderItem); err != nil { + return nil, fmt.Errorf("failed to create order item: %w", err) + } + } + + orderWithRelations, err := p.orderRepo.GetWithRelations(ctx, order.ID) + if err != nil { + return nil, fmt.Errorf("failed to retrieve created order: %w", err) + } + + response := mappers.OrderEntityToResponse(orderWithRelations) + return response, nil +} + +func (p *OrderProcessorImpl) AddToOrder(ctx context.Context, orderID uuid.UUID, req *models.AddToOrderRequest) (*models.AddToOrderResponse, error) { + order, err := p.orderRepo.GetByID(ctx, orderID) + if err != nil { + return nil, fmt.Errorf("order not found: %w", err) + } + + if order.IsVoid { + return nil, fmt.Errorf("cannot modify voided order") + } + + if order.PaymentStatus == entities.PaymentStatusCompleted { + return nil, fmt.Errorf("cannot modify fully paid order") + } + + // Get outlet information for tax rate + outlet, err := p.outletRepo.GetByID(ctx, order.OutletID) + if err != nil { + return nil, fmt.Errorf("outlet not found: %w", err) + } + + var newSubtotal, newTotalCost float64 + var addedOrderItems []*entities.OrderItem + + for _, itemReq := range req.OrderItems { + product, err := p.productRepo.GetByID(ctx, itemReq.ProductID) + if err != nil { + return nil, fmt.Errorf("product not found: %w", err) + } + + // Use product price from database + unitPrice := product.Price + unitCost := product.Cost + + // Handle product variant if specified + if itemReq.ProductVariantID != nil { + variant, err := p.productVariantRepo.GetByID(ctx, *itemReq.ProductVariantID) + if err != nil { + return nil, fmt.Errorf("product variant not found: %w", err) + } + + // Verify variant belongs to the product + if variant.ProductID != itemReq.ProductID { + return nil, fmt.Errorf("product variant does not belong to the specified product") + } + + // Apply price modifier + unitPrice += variant.PriceModifier + // Use variant cost if available, otherwise use product cost + if variant.Cost > 0 { + unitCost = variant.Cost + } + } + + itemTotalPrice := float64(itemReq.Quantity) * unitPrice + itemTotalCost := float64(itemReq.Quantity) * unitCost + + newSubtotal += itemTotalPrice + newTotalCost += itemTotalCost + + orderItem := &entities.OrderItem{ + OrderID: orderID, + ProductID: itemReq.ProductID, + ProductVariantID: itemReq.ProductVariantID, + Quantity: itemReq.Quantity, + UnitPrice: unitPrice, // Use price from database + TotalPrice: itemTotalPrice, + UnitCost: unitCost, + TotalCost: itemTotalCost, + Modifiers: entities.Modifiers(itemReq.Modifiers), + Notes: itemReq.Notes, + Metadata: entities.Metadata(itemReq.Metadata), + Status: entities.OrderItemStatusPending, + } + + addedOrderItems = append(addedOrderItems, orderItem) + } + + order.Subtotal += newSubtotal + order.TotalCost += newTotalCost + // Recalculate tax amount using outlet's tax rate + order.TaxAmount = order.Subtotal * outlet.TaxRate + order.TotalAmount = order.Subtotal + order.TaxAmount - order.DiscountAmount + + if req.Metadata != nil { + if order.Metadata == nil { + order.Metadata = make(entities.Metadata) + } + for k, v := range req.Metadata { + order.Metadata[k] = v + } + } + + if err := p.orderRepo.Update(ctx, order); err != nil { + return nil, fmt.Errorf("failed to update order: %w", err) + } + + var addedItemResponses []models.OrderItemResponse + for _, orderItem := range addedOrderItems { + if err := p.orderItemRepo.Create(ctx, orderItem); err != nil { + return nil, fmt.Errorf("failed to create order item: %w", err) + } + + itemResponse := models.OrderItemResponse{ + ID: orderItem.ID, + OrderID: orderItem.OrderID, + ProductID: orderItem.ProductID, + ProductVariantID: orderItem.ProductVariantID, + Quantity: orderItem.Quantity, + UnitPrice: orderItem.UnitPrice, + TotalPrice: orderItem.TotalPrice, + UnitCost: orderItem.UnitCost, + TotalCost: orderItem.TotalCost, + RefundAmount: orderItem.RefundAmount, + RefundQuantity: orderItem.RefundQuantity, + IsPartiallyRefunded: orderItem.IsPartiallyRefunded, + IsFullyRefunded: orderItem.IsFullyRefunded, + RefundReason: orderItem.RefundReason, + RefundedAt: orderItem.RefundedAt, + RefundedBy: orderItem.RefundedBy, + Modifiers: []map[string]interface{}(orderItem.Modifiers), + Notes: orderItem.Notes, + Metadata: map[string]interface{}(orderItem.Metadata), + Status: constants.OrderItemStatus(orderItem.Status), + CreatedAt: orderItem.CreatedAt, + UpdatedAt: orderItem.UpdatedAt, + } + addedItemResponses = append(addedItemResponses, itemResponse) + } + + orderWithRelations, err := p.orderRepo.GetWithRelations(ctx, orderID) + if err != nil { + return nil, fmt.Errorf("failed to retrieve updated order: %w", err) + } + + updatedOrderResponse := mappers.OrderEntityToResponse(orderWithRelations) + + return &models.AddToOrderResponse{ + OrderID: orderID, + OrderNumber: order.OrderNumber, + AddedItems: addedItemResponses, + UpdatedOrder: *updatedOrderResponse, + }, nil +} + +func (p *OrderProcessorImpl) UpdateOrder(ctx context.Context, id uuid.UUID, req *models.UpdateOrderRequest) (*models.OrderResponse, error) { + // Get existing order + order, err := p.orderRepo.GetByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("order not found: %w", err) + } + + // Check if order can be modified + if order.IsVoid { + return nil, fmt.Errorf("cannot modify voided order") + } + + // Apply updates + if req.TableNumber != nil { + order.TableNumber = req.TableNumber + } + if req.Status != nil { + order.Status = entities.OrderStatus(*req.Status) + } + if req.DiscountAmount != nil { + order.DiscountAmount = *req.DiscountAmount + // Recalculate total amount + order.TotalAmount = order.Subtotal + order.TaxAmount - order.DiscountAmount + } + if req.Metadata != nil { + if order.Metadata == nil { + order.Metadata = make(entities.Metadata) + } + for k, v := range req.Metadata { + order.Metadata[k] = v + } + } + + // Update order + if err := p.orderRepo.Update(ctx, order); err != nil { + return nil, fmt.Errorf("failed to update order: %w", err) + } + + // Get updated order with relations + orderWithRelations, err := p.orderRepo.GetWithRelations(ctx, id) + if err != nil { + return nil, fmt.Errorf("failed to retrieve updated order: %w", err) + } + + response := mappers.OrderEntityToResponse(orderWithRelations) + return response, nil +} + +func (p *OrderProcessorImpl) GetOrderByID(ctx context.Context, id uuid.UUID) (*models.OrderResponse, error) { + order, err := p.orderRepo.GetWithRelations(ctx, id) + if err != nil { + return nil, fmt.Errorf("order not found: %w", err) + } + + response := mappers.OrderEntityToResponse(order) + return response, nil +} + +func (p *OrderProcessorImpl) ListOrders(ctx context.Context, req *models.ListOrdersRequest) (*models.ListOrdersResponse, error) { + filters := make(map[string]interface{}) + if req.OrganizationID != nil { + filters["organization_id"] = *req.OrganizationID + } + if req.OutletID != nil { + filters["outlet_id"] = *req.OutletID + } + if req.UserID != nil { + filters["user_id"] = *req.UserID + } + if req.CustomerID != nil { + filters["customer_id"] = *req.CustomerID + } + if req.OrderType != nil { + filters["order_type"] = string(*req.OrderType) + } + if req.Status != nil { + filters["status"] = string(*req.Status) + } + if req.PaymentStatus != nil { + filters["payment_status"] = string(*req.PaymentStatus) + } + if req.IsVoid != nil { + filters["is_void"] = *req.IsVoid + } + if req.IsRefund != nil { + filters["is_refund"] = *req.IsRefund + } + if req.DateFrom != nil { + filters["date_from"] = *req.DateFrom + } + if req.DateTo != nil { + filters["date_to"] = *req.DateTo + } + if req.Search != "" { + filters["search"] = req.Search + } + + offset := (req.Page - 1) * req.Limit + orders, total, err := p.orderRepo.List(ctx, filters, req.Limit, offset) + if err != nil { + return nil, fmt.Errorf("failed to list orders: %w", err) + } + + // Convert to responses + orderResponses := make([]models.OrderResponse, len(orders)) + for i, order := range orders { + response := mappers.OrderEntityToResponse(order) + if response != nil { + orderResponses[i] = *response + } + } + + // Calculate total pages + totalPages := int(total) / req.Limit + if int(total)%req.Limit > 0 { + totalPages++ + } + + return &models.ListOrdersResponse{ + Orders: orderResponses, + TotalCount: int(total), + Page: req.Page, + Limit: req.Limit, + TotalPages: totalPages, + }, nil +} + +func (p *OrderProcessorImpl) VoidOrder(ctx context.Context, req *models.VoidOrderRequest, voidedBy uuid.UUID) error { + if req.OrderID != req.OrderID { + return fmt.Errorf("order ID mismatch: path parameter does not match request body") + } + + order, err := p.orderRepo.GetByID(ctx, req.OrderID) + if err != nil { + return fmt.Errorf("order not found: %w", err) + } + + if order.IsVoid { + return fmt.Errorf("order is already voided") + } + + if order.PaymentStatus == entities.PaymentStatusCompleted { + return fmt.Errorf("cannot void fully paid order") + } + + if req.Type == "ALL" { + // Update order status to cancelled and mark as voided in a single transaction + if err := p.orderRepo.VoidOrderWithStatus(ctx, req.OrderID, entities.OrderStatusCancelled, req.Reason, voidedBy); err != nil { + return fmt.Errorf("failed to void order: %w", err) + } + } else if req.Type == "ITEM" { + if len(req.Items) == 0 { + return fmt.Errorf("items list is required when voiding specific items") + } + + var totalVoidedAmount float64 + var totalVoidedCost float64 + + for _, itemVoid := range req.Items { + orderItemID := itemVoid.OrderItemID + + orderItem, err := p.orderItemRepo.GetByID(ctx, orderItemID) + if err != nil { + return fmt.Errorf("order item not found: %w", err) + } + + // Verify the order item belongs to this order + if orderItem.OrderID != req.OrderID { + return fmt.Errorf("order item does not belong to this order") + } + + // Validate void quantity + if itemVoid.Quantity > orderItem.Quantity { + return fmt.Errorf("void quantity cannot exceed original quantity for item %d", itemVoid.OrderItemID) + } + + // Calculate voided amounts + voidedAmount := float64(itemVoid.Quantity) * orderItem.UnitPrice + voidedCost := float64(itemVoid.Quantity) * orderItem.UnitCost + + totalVoidedAmount += voidedAmount + totalVoidedCost += voidedCost + + // Void the order item + if err := p.orderItemRepo.VoidOrderItem(ctx, orderItemID, itemVoid.Quantity, req.Reason, voidedBy); err != nil { + return fmt.Errorf("failed to void order item %d: %w", itemVoid.OrderItemID, err) + } + } + + // Get outlet information for tax rate + outlet, err := p.outletRepo.GetByID(ctx, order.OutletID) + if err != nil { + return fmt.Errorf("outlet not found: %w", err) + } + + // Update order totals + order.Subtotal -= totalVoidedAmount + order.TotalCost -= totalVoidedCost + order.TaxAmount = order.Subtotal * outlet.TaxRate // Recalculate tax using outlet's tax rate + order.TotalAmount = order.Subtotal + order.TaxAmount - order.DiscountAmount + + // Update the order + if err := p.orderRepo.Update(ctx, order); err != nil { + return fmt.Errorf("failed to update order totals: %w", err) + } + + // Check if all items are voided, then void the entire order + remainingItems, err := p.orderItemRepo.GetByOrderID(ctx, req.OrderID) + if err != nil { + return fmt.Errorf("failed to get remaining order items: %w", err) + } + + allItemsVoided := true + for _, item := range remainingItems { + if item.Quantity > 0 { + allItemsVoided = false + break + } + } + + if allItemsVoided { + // Update order status to cancelled and mark as voided when all items are voided + if err := p.orderRepo.VoidOrderWithStatus(ctx, req.OrderID, entities.OrderStatusCancelled, req.Reason, voidedBy); err != nil { + return fmt.Errorf("failed to void order after all items voided: %w", err) + } + } + } else { + return fmt.Errorf("invalid void type: must be 'ALL' or 'ITEM'") + } + + return nil +} + +func (p *OrderProcessorImpl) RefundOrder(ctx context.Context, id uuid.UUID, req *models.RefundOrderRequest, refundedBy uuid.UUID) error { + order, err := p.orderRepo.GetWithRelations(ctx, id) + if err != nil { + return fmt.Errorf("order not found: %w", err) + } + + // Check if order can be refunded + if order.IsRefund { + return fmt.Errorf("order is already refunded") + } + + if order.PaymentStatus != entities.PaymentStatusCompleted { + return fmt.Errorf("order is not paid, cannot refund") + } + + reason := "No reason provided" + if req.Reason != nil { + reason = *req.Reason + } + + // Process refund based on request type + if req.RefundAmount != nil { + // Full or partial refund by amount + if *req.RefundAmount > order.TotalAmount { + return fmt.Errorf("refund amount cannot exceed order total") + } + + // Update order refund amount + order.RefundAmount = *req.RefundAmount + if err := p.orderRepo.Update(ctx, order); err != nil { + return fmt.Errorf("failed to update order refund amount: %w", err) + } + + // Mark order as refunded + if err := p.orderRepo.RefundOrder(ctx, id, reason, refundedBy); err != nil { + return fmt.Errorf("failed to mark order as refunded: %w", err) + } + + } else if len(req.OrderItems) > 0 { + // Refund by specific items + totalRefundAmount := float64(0) + + for _, itemRefund := range req.OrderItems { + // Get order item + orderItem, err := p.orderItemRepo.GetByID(ctx, itemRefund.OrderItemID) + if err != nil { + return fmt.Errorf("order item not found: %w", err) + } + + if orderItem.OrderID != id { + return fmt.Errorf("order item does not belong to this order") + } + + // Calculate refund amount for this item + refundQuantity := itemRefund.RefundQuantity + if refundQuantity == 0 { + refundQuantity = orderItem.Quantity + } + + if refundQuantity > orderItem.Quantity { + return fmt.Errorf("refund quantity cannot exceed original quantity") + } + + refundAmount := float64(refundQuantity) * orderItem.UnitPrice + if itemRefund.RefundAmount != nil { + refundAmount = *itemRefund.RefundAmount + } + + // Process item refund + itemReason := reason + if itemRefund.Reason != nil { + itemReason = *itemRefund.Reason + } + + if err := p.orderItemRepo.RefundOrderItem(ctx, itemRefund.OrderItemID, refundQuantity, refundAmount, itemReason, refundedBy); err != nil { + return fmt.Errorf("failed to refund order item: %w", err) + } + + totalRefundAmount += refundAmount + } + + // Update order refund amount + order.RefundAmount = totalRefundAmount + if err := p.orderRepo.Update(ctx, order); err != nil { + return fmt.Errorf("failed to update order refund amount: %w", err) + } + + // Mark order as refunded + if err := p.orderRepo.RefundOrder(ctx, id, reason, refundedBy); err != nil { + return fmt.Errorf("failed to mark order as refunded: %w", err) + } + } + + return nil +} + +func (p *OrderProcessorImpl) CreatePayment(ctx context.Context, req *models.CreatePaymentRequest) (*models.PaymentResponse, error) { + order, err := p.orderRepo.GetByID(ctx, req.OrderID) + if err != nil { + return nil, fmt.Errorf("order not found: %w", err) + } + + if order.IsVoid { + return nil, fmt.Errorf("cannot process payment for voided order") + } + + if order.PaymentStatus == entities.PaymentStatusCompleted { + return nil, fmt.Errorf("order is already fully paid") + } + + _, err = p.paymentMethodRepo.GetByID(ctx, req.PaymentMethodID) + if err != nil { + return nil, fmt.Errorf("payment method not found: %w", err) + } + + totalPaid, err := p.paymentRepo.GetTotalPaidByOrderID(ctx, req.OrderID) + if err != nil { + return nil, fmt.Errorf("failed to get total paid: %w", err) + } + + remainingAmount := order.TotalAmount - totalPaid + if req.Amount > remainingAmount { + return nil, fmt.Errorf("payment amount exceeds remaining balance") + } + + payment := &entities.Payment{ + OrderID: req.OrderID, + PaymentMethodID: req.PaymentMethodID, + Amount: req.Amount, + Status: entities.PaymentTransactionStatusCompleted, + TransactionID: req.TransactionID, + SplitNumber: req.SplitNumber, + SplitTotal: req.SplitTotal, + SplitDescription: req.SplitDescription, + Metadata: entities.Metadata(req.Metadata), + } + + if err := p.paymentRepo.Create(ctx, payment); err != nil { + return nil, fmt.Errorf("failed to create payment: %w", err) + } + + if len(req.PaymentOrderItems) > 0 { + for _, itemPayment := range req.PaymentOrderItems { + paymentOrderItem := &entities.PaymentOrderItem{ + PaymentID: payment.ID, + OrderItemID: itemPayment.OrderItemID, + Amount: itemPayment.Amount, + } + + fmt.Println(paymentOrderItem) + // TODO: Create payment order item in database + // This would require a PaymentOrderItemRepository + } + } + + // Update order payment status if fully paid + newTotalPaid := totalPaid + req.Amount + orderJustCompleted := false + if newTotalPaid >= order.TotalAmount { + if order.PaymentStatus != entities.PaymentStatusCompleted { + orderJustCompleted = true + } + if err := p.orderRepo.UpdatePaymentStatus(ctx, req.OrderID, entities.PaymentStatusCompleted); err != nil { + return nil, fmt.Errorf("failed to update order payment status: %w", err) + } + // Set order status to completed when fully paid + if err := p.orderRepo.UpdateStatus(ctx, req.OrderID, entities.OrderStatusCompleted); err != nil { + return nil, fmt.Errorf("failed to update order status: %w", err) + } + } else { + if err := p.orderRepo.UpdatePaymentStatus(ctx, req.OrderID, entities.PaymentStatusPartiallyRefunded); err != nil { + return nil, fmt.Errorf("failed to update order payment status: %w", err) + } + } + + if orderJustCompleted { + orderItems, err := p.orderItemRepo.GetByOrderID(ctx, req.OrderID) + if err != nil { + return nil, fmt.Errorf("failed to get order items for inventory adjustment: %w", err) + } + for _, item := range orderItems { + if _, err := p.inventoryRepo.AdjustQuantity(ctx, item.ProductID, order.OutletID, -item.Quantity); err != nil { + return nil, fmt.Errorf("failed to adjust inventory for product %s: %w", item.ProductID, err) + } + } + } + + // Get payment with relations for response + paymentWithRelations, err := p.paymentRepo.GetByID(ctx, payment.ID) + if err != nil { + return nil, fmt.Errorf("failed to retrieve created payment: %w", err) + } + + response := mappers.PaymentEntityToResponse(paymentWithRelations) + return response, nil +} + +func (p *OrderProcessorImpl) RefundPayment(ctx context.Context, paymentID uuid.UUID, refundAmount float64, reason string, refundedBy uuid.UUID) error { + // Get payment + payment, err := p.paymentRepo.GetByID(ctx, paymentID) + if err != nil { + return fmt.Errorf("payment not found: %w", err) + } + + // Check if payment can be refunded + if payment.Status != entities.PaymentTransactionStatusCompleted { + return fmt.Errorf("payment is not completed, cannot refund") + } + + if refundAmount > payment.Amount { + return fmt.Errorf("refund amount cannot exceed payment amount") + } + + // Process refund + if err := p.paymentRepo.RefundPayment(ctx, paymentID, refundAmount, reason, refundedBy); err != nil { + return fmt.Errorf("failed to refund payment: %w", err) + } + + // Update order refund amount + order, err := p.orderRepo.GetByID(ctx, payment.OrderID) + if err != nil { + return fmt.Errorf("failed to get order: %w", err) + } + + order.RefundAmount += refundAmount + if err := p.orderRepo.Update(ctx, order); err != nil { + return fmt.Errorf("failed to update order refund amount: %w", err) + } + + return nil +} + +func (p *OrderProcessorImpl) SetOrderCustomer(ctx context.Context, orderID uuid.UUID, req *models.SetOrderCustomerRequest, organizationID uuid.UUID) (*models.SetOrderCustomerResponse, error) { + // Get the order + order, err := p.orderRepo.GetByID(ctx, orderID) + if err != nil { + return nil, fmt.Errorf("order not found: %w", err) + } + + // Verify order belongs to the organization + if order.OrganizationID != organizationID { + return nil, fmt.Errorf("order does not belong to the organization") + } + + // Check if order status is pending (only pending orders can have customer set) + if order.Status != entities.OrderStatusPending { + return nil, fmt.Errorf("customer can only be set for pending orders") + } + + // Verify customer exists and belongs to the organization + customer, err := p.customerRepo.GetByIDAndOrganization(ctx, req.CustomerID, organizationID) + if err != nil { + return nil, fmt.Errorf("customer not found or does not belong to the organization: %w", err) + } + + // Update order with customer ID + order.CustomerID = &req.CustomerID + if err := p.orderRepo.Update(ctx, order); err != nil { + return nil, fmt.Errorf("failed to update order with customer: %w", err) + } + + response := &models.SetOrderCustomerResponse{ + OrderID: orderID, + CustomerID: req.CustomerID, + Message: fmt.Sprintf("Customer '%s' successfully set for order", customer.Name), + } + + return response, nil +} diff --git a/internal/processor/organization_processor.go b/internal/processor/organization_processor.go new file mode 100644 index 0000000..b87aae6 --- /dev/null +++ b/internal/processor/organization_processor.go @@ -0,0 +1,186 @@ +package processor + +import ( + "context" + "fmt" + + "golang.org/x/crypto/bcrypt" + + "apskel-pos-be/internal/entities" + "apskel-pos-be/internal/mappers" + "apskel-pos-be/internal/models" + + "github.com/google/uuid" +) + +type OrganizationProcessor interface { + CreateOrganization(ctx context.Context, req *models.CreateOrganizationRequest) (*models.CreateOrganizationResponse, error) + UpdateOrganization(ctx context.Context, id uuid.UUID, req *models.UpdateOrganizationRequest) (*models.OrganizationResponse, error) + DeleteOrganization(ctx context.Context, id uuid.UUID) error + GetOrganizationByID(ctx context.Context, id uuid.UUID) (*models.OrganizationResponse, error) + ListOrganizations(ctx context.Context, filters map[string]interface{}, page, limit int) ([]models.OrganizationResponse, int, error) +} + +type OrganizationProcessorImpl struct { + organizationRepo OrganizationRepository + outletRepo OutletRepository + userRepo UserRepository +} + +func NewOrganizationProcessorImpl( + organizationRepo OrganizationRepository, + outletRepo OutletRepository, + userRepo UserRepository, +) *OrganizationProcessorImpl { + return &OrganizationProcessorImpl{ + organizationRepo: organizationRepo, + outletRepo: outletRepo, + userRepo: userRepo, + } +} + +func (p *OrganizationProcessorImpl) CreateOrganization(ctx context.Context, req *models.CreateOrganizationRequest) (*models.CreateOrganizationResponse, error) { + if req.OrganizationEmail != nil && *req.OrganizationEmail != "" { + existingOrg, err := p.organizationRepo.GetByEmail(ctx, *req.OrganizationEmail) + if err == nil && existingOrg != nil { + return nil, fmt.Errorf("organization with email %s already exists", *req.OrganizationEmail) + } + } + + existingUser, err := p.userRepo.GetByEmail(ctx, req.AdminEmail) + if err == nil && existingUser != nil { + return nil, fmt.Errorf("user with email %s already exists", req.AdminEmail) + } + + organizationEntity := &entities.Organization{ + Name: req.OrganizationName, + Email: req.OrganizationEmail, + PhoneNumber: req.OrganizationPhoneNumber, + PlanType: string(req.PlanType), + } + + err = p.organizationRepo.Create(ctx, organizationEntity) + if err != nil { + return nil, fmt.Errorf("failed to create organization: %w", err) + } + + passwordHash, err := bcrypt.GenerateFromPassword([]byte(req.AdminPassword), bcrypt.DefaultCost) + if err != nil { + return nil, fmt.Errorf("failed to hash password: %w", err) + } + + adminUserEntity := &entities.User{ + OrganizationID: organizationEntity.ID, + Name: req.AdminName, + Email: req.AdminEmail, + PasswordHash: string(passwordHash), + Role: entities.RoleAdmin, + IsActive: true, + } + + err = p.userRepo.Create(ctx, adminUserEntity) + if err != nil { + return nil, fmt.Errorf("failed to create admin user: %w", err) + } + + defaultOutletEntity := &entities.Outlet{ + OrganizationID: organizationEntity.ID, + Name: req.OutletName, + Address: req.OutletAddress, + Timezone: req.OutletTimezone, + Currency: req.OutletCurrency, + TaxRate: 0.0, + IsActive: true, + } + + err = p.outletRepo.Create(ctx, defaultOutletEntity) + if err != nil { + return nil, fmt.Errorf("failed to create default outlet: %w", err) + } + + organizationResponse := mappers.OrganizationEntityToResponse(organizationEntity) + adminUserResponse := mappers.UserEntityToResponse(adminUserEntity) + outletResponse := mappers.OutletEntityToResponse(defaultOutletEntity) + + return &models.CreateOrganizationResponse{ + Organization: organizationResponse, + AdminUser: adminUserResponse, + DefaultOutlet: outletResponse, + }, nil +} + +func (p *OrganizationProcessorImpl) UpdateOrganization(ctx context.Context, id uuid.UUID, req *models.UpdateOrganizationRequest) (*models.OrganizationResponse, error) { + existingOrg, err := p.organizationRepo.GetByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("organization not found: %w", err) + } + + if req.Email != nil && (existingOrg.Email == nil || *req.Email != *existingOrg.Email) { + existingOrgByEmail, err := p.organizationRepo.GetByEmail(ctx, *req.Email) + if err == nil && existingOrgByEmail != nil && existingOrgByEmail.ID != id { + return nil, fmt.Errorf("organization with email %s already exists", *req.Email) + } + } + + if req.Name != nil { + existingOrg.Name = *req.Name + } + if req.Email != nil { + existingOrg.Email = req.Email + } + if req.PhoneNumber != nil { + existingOrg.PhoneNumber = req.PhoneNumber + } + if req.PlanType != nil { + existingOrg.PlanType = string(*req.PlanType) + } + + err = p.organizationRepo.Update(ctx, existingOrg) + if err != nil { + return nil, fmt.Errorf("failed to update organization: %w", err) + } + + return mappers.OrganizationEntityToResponse(existingOrg), nil +} + +func (p *OrganizationProcessorImpl) DeleteOrganization(ctx context.Context, id uuid.UUID) error { + _, err := p.organizationRepo.GetByID(ctx, id) + if err != nil { + return fmt.Errorf("organization not found: %w", err) + } + + err = p.organizationRepo.Delete(ctx, id) + if err != nil { + return fmt.Errorf("failed to delete organization: %w", err) + } + + return nil +} + +func (p *OrganizationProcessorImpl) GetOrganizationByID(ctx context.Context, id uuid.UUID) (*models.OrganizationResponse, error) { + organization, err := p.organizationRepo.GetByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("organization not found: %w", err) + } + + return mappers.OrganizationEntityToResponse(organization), nil +} + +func (p *OrganizationProcessorImpl) ListOrganizations(ctx context.Context, filters map[string]interface{}, page, limit int) ([]models.OrganizationResponse, int, error) { + offset := (page - 1) * limit + + organizations, totalCount, err := p.organizationRepo.List(ctx, filters, limit, offset) + if err != nil { + return nil, 0, fmt.Errorf("failed to get organizations: %w", err) + } + + responses := make([]models.OrganizationResponse, len(organizations)) + for i, org := range organizations { + response := mappers.OrganizationEntityToResponse(org) + if response != nil { + responses[i] = *response + } + } + + return responses, int(totalCount), nil +} diff --git a/internal/processor/organization_repository.go b/internal/processor/organization_repository.go new file mode 100644 index 0000000..ce766ec --- /dev/null +++ b/internal/processor/organization_repository.go @@ -0,0 +1,20 @@ +package processor + +import ( + "apskel-pos-be/internal/entities" + "context" + "github.com/google/uuid" +) + +type OrganizationRepository interface { + Create(ctx context.Context, org *entities.Organization) error + GetByID(ctx context.Context, id uuid.UUID) (*entities.Organization, error) + GetWithOutlets(ctx context.Context, id uuid.UUID) (*entities.Organization, error) + GetByPlanType(ctx context.Context, planType string) ([]*entities.Organization, error) + UpdatePlanType(ctx context.Context, id uuid.UUID, planType string) error + Update(ctx context.Context, org *entities.Organization) error + Delete(ctx context.Context, id uuid.UUID) error + List(ctx context.Context, filters map[string]interface{}, limit, offset int) ([]*entities.Organization, int64, error) + Count(ctx context.Context, filters map[string]interface{}) (int64, error) + GetByEmail(ctx context.Context, email string) (*entities.Organization, error) +} diff --git a/internal/processor/outlet_processor.go b/internal/processor/outlet_processor.go new file mode 100644 index 0000000..a33882d --- /dev/null +++ b/internal/processor/outlet_processor.go @@ -0,0 +1,149 @@ +package processor + +import ( + "apskel-pos-be/internal/appcontext" + "apskel-pos-be/internal/entities" + "apskel-pos-be/internal/mappers" + "apskel-pos-be/internal/models" + "apskel-pos-be/internal/repository" + "context" + "fmt" + + "github.com/google/uuid" +) + +type OutletProcessor interface { + ListOutletsByOrganization(ctx context.Context, organizationID uuid.UUID, page, limit int) ([]*models.OutletResponse, int64, error) + GetOutletByID(ctx context.Context, organizationID uuid.UUID, outletID uuid.UUID) (*models.OutletResponse, error) + CreateOutlet(ctx context.Context, req *models.CreateOutletRequest) (*models.OutletResponse, error) + UpdateOutlet(ctx context.Context, outletID uuid.UUID, req *models.UpdateOutletRequest) (*models.OutletResponse, error) + DeleteOutlet(ctx context.Context, outletID uuid.UUID) error +} + +type OutletProcessorImpl struct { + outletRepo *repository.OutletRepositoryImpl +} + +func NewOutletProcessorImpl(outletRepo *repository.OutletRepositoryImpl) *OutletProcessorImpl { + return &OutletProcessorImpl{ + outletRepo: outletRepo, + } +} + +func (p *OutletProcessorImpl) ListOutletsByOrganization(ctx context.Context, organizationID uuid.UUID, page, limit int) ([]*models.OutletResponse, int64, error) { + + offset := (page - 1) * limit + + // Get outlets with pagination + outlets, total, err := p.outletRepo.GetByOrganizationIDWithPagination(ctx, organizationID, limit, offset) + if err != nil { + return nil, 0, fmt.Errorf("failed to get outlets: %w", err) + } + + // Convert to response models + responses := make([]*models.OutletResponse, len(outlets)) + for i, outlet := range outlets { + responses[i] = mappers.OutletEntityToResponse(outlet) + } + + return responses, total, nil +} + +func (p *OutletProcessorImpl) GetOutletByID(ctx context.Context, organizationID, outletID uuid.UUID) (*models.OutletResponse, error) { + outlet, err := p.outletRepo.GetByID(ctx, outletID) + if err != nil { + return nil, fmt.Errorf("outlet not found: %w", err) + } + + if outlet.OrganizationID != organizationID { + return nil, fmt.Errorf("outlet does not belong to the organization") + } + + response := mappers.OutletEntityToResponse(outlet) + return response, nil +} + +func (p *OutletProcessorImpl) CreateOutlet(ctx context.Context, req *models.CreateOutletRequest) (*models.OutletResponse, error) { + // Get organization ID from context + contextInfo := appcontext.FromContext(ctx) + if contextInfo.OrganizationID == uuid.Nil { + return nil, fmt.Errorf("organization ID not found in context") + } + + // Set organization ID from context + req.OrganizationID = contextInfo.OrganizationID + + // Create outlet entity + outlet := &entities.Outlet{ + OrganizationID: req.OrganizationID, + Name: req.Name, + Address: &req.Address, + Currency: string(req.Currency), + TaxRate: req.TaxRate, + IsActive: true, + } + + err := p.outletRepo.Create(ctx, outlet) + if err != nil { + return nil, fmt.Errorf("failed to create outlet: %w", err) + } + + response := mappers.OutletEntityToResponse(outlet) + return response, nil +} + +func (p *OutletProcessorImpl) UpdateOutlet(ctx context.Context, outletID uuid.UUID, req *models.UpdateOutletRequest) (*models.OutletResponse, error) { + outlet, err := p.outletRepo.GetByID(ctx, outletID) + if err != nil { + return nil, fmt.Errorf("outlet not found: %w", err) + } + + if outlet.OrganizationID != req.OrganizationID { + return nil, fmt.Errorf("outlet does not belong to the organization") + } + + if req.Name != nil { + outlet.Name = *req.Name + } + if req.Address != nil { + outlet.Address = req.Address + } + if req.TaxRate != nil { + outlet.TaxRate = *req.TaxRate + } + if req.IsActive != nil { + outlet.IsActive = *req.IsActive + } + + err = p.outletRepo.Update(ctx, outlet) + if err != nil { + return nil, fmt.Errorf("failed to update outlet: %w", err) + } + + response := mappers.OutletEntityToResponse(outlet) + return response, nil +} + +func (p *OutletProcessorImpl) DeleteOutlet(ctx context.Context, outletID uuid.UUID) error { + contextInfo := appcontext.FromContext(ctx) + if contextInfo.OrganizationID == uuid.Nil { + return fmt.Errorf("organization ID not found in context") + } + + // Get existing outlet + outlet, err := p.outletRepo.GetByID(ctx, outletID) + if err != nil { + return fmt.Errorf("outlet not found: %w", err) + } + + if outlet.OrganizationID != contextInfo.OrganizationID { + return fmt.Errorf("outlet does not belong to the organization") + } + + err = p.outletRepo.Delete(ctx, outletID) + if err != nil { + return fmt.Errorf("failed to delete outlet: %w", err) + } + + return nil +} diff --git a/internal/processor/outlet_repository.go b/internal/processor/outlet_repository.go new file mode 100644 index 0000000..abd1f94 --- /dev/null +++ b/internal/processor/outlet_repository.go @@ -0,0 +1,20 @@ +package processor + +import ( + "apskel-pos-be/internal/entities" + "context" + "github.com/google/uuid" +) + +type OutletRepository interface { + Create(ctx context.Context, outlet *entities.Outlet) error + GetByID(ctx context.Context, id uuid.UUID) (*entities.Outlet, error) + GetWithOrders(ctx context.Context, id uuid.UUID) (*entities.Outlet, error) + GetByOrganizationID(ctx context.Context, organizationID uuid.UUID) ([]*entities.Outlet, error) + GetByOrganizationIDWithPagination(ctx context.Context, organizationID uuid.UUID, limit, offset int) ([]*entities.Outlet, int64, error) + Update(ctx context.Context, outlet *entities.Outlet) error + Delete(ctx context.Context, id uuid.UUID) error + UpdateActiveStatus(ctx context.Context, id uuid.UUID, isActive bool) error + List(ctx context.Context, filters map[string]interface{}, limit, offset int) ([]*entities.Outlet, int64, error) + Count(ctx context.Context, filters map[string]interface{}) (int64, error) +} diff --git a/internal/processor/outlet_setting_processor.go b/internal/processor/outlet_setting_processor.go new file mode 100644 index 0000000..37924ef --- /dev/null +++ b/internal/processor/outlet_setting_processor.go @@ -0,0 +1,232 @@ +package processor + +import ( + "apskel-pos-be/internal/constants" + "apskel-pos-be/internal/entities" + "apskel-pos-be/internal/models" + "apskel-pos-be/internal/repository" + "context" + "fmt" + + "github.com/google/uuid" +) + +type OutletSettingProcessorImpl struct { + outletSettingRepo *repository.OutletSettingRepositoryImpl + outletRepo *repository.OutletRepositoryImpl +} + +func NewOutletSettingProcessorImpl( + outletSettingRepo *repository.OutletSettingRepositoryImpl, + outletRepo *repository.OutletRepositoryImpl, +) *OutletSettingProcessorImpl { + return &OutletSettingProcessorImpl{ + outletSettingRepo: outletSettingRepo, + outletRepo: outletRepo, + } +} + +func (p *OutletSettingProcessorImpl) CreateSetting(ctx context.Context, req *models.CreateOutletSettingRequest) (*models.OutletSettingResponse, error) { + // Check if outlet exists + _, err := p.outletRepo.GetByID(ctx, req.OutletID) + if err != nil { + return nil, fmt.Errorf("outlet not found: %w", err) + } + + // Check if setting already exists + existingSetting, err := p.outletSettingRepo.GetByOutletIDAndKey(ctx, req.OutletID, req.Key) + if err == nil && existingSetting != nil { + return nil, fmt.Errorf("setting with key '%s' already exists for this outlet", req.Key) + } + + setting := &entities.OutletSetting{ + OutletID: req.OutletID, + Key: req.Key, + Value: req.Value, + } + + err = p.outletSettingRepo.Create(ctx, setting) + if err != nil { + return nil, fmt.Errorf("failed to create setting: %w", err) + } + + return &models.OutletSettingResponse{ + ID: setting.ID, + OutletID: setting.OutletID, + Key: setting.Key, + Value: setting.Value, + CreatedAt: setting.CreatedAt, + UpdatedAt: setting.UpdatedAt, + }, nil +} + +func (p *OutletSettingProcessorImpl) UpdateSetting(ctx context.Context, outletID uuid.UUID, key string, req *models.UpdateOutletSettingRequest) (*models.OutletSettingResponse, error) { + // Check if outlet exists + _, err := p.outletRepo.GetByID(ctx, outletID) + if err != nil { + return nil, fmt.Errorf("outlet not found: %w", err) + } + + // Get existing setting + setting, err := p.outletSettingRepo.GetByOutletIDAndKey(ctx, outletID, key) + if err != nil { + return nil, fmt.Errorf("setting not found: %w", err) + } + + // Update setting + setting.Value = req.Value + err = p.outletSettingRepo.Update(ctx, setting) + if err != nil { + return nil, fmt.Errorf("failed to update setting: %w", err) + } + + return &models.OutletSettingResponse{ + ID: setting.ID, + OutletID: setting.OutletID, + Key: setting.Key, + Value: setting.Value, + CreatedAt: setting.CreatedAt, + UpdatedAt: setting.UpdatedAt, + }, nil +} + +func (p *OutletSettingProcessorImpl) GetSetting(ctx context.Context, outletID uuid.UUID, key string) (*models.OutletSettingResponse, error) { + // Check if outlet exists + _, err := p.outletRepo.GetByID(ctx, outletID) + if err != nil { + return nil, fmt.Errorf("outlet not found: %w", err) + } + + setting, err := p.outletSettingRepo.GetByOutletIDAndKey(ctx, outletID, key) + if err != nil { + return nil, fmt.Errorf("setting not found: %w", err) + } + + return &models.OutletSettingResponse{ + ID: setting.ID, + OutletID: setting.OutletID, + Key: setting.Key, + Value: setting.Value, + CreatedAt: setting.CreatedAt, + UpdatedAt: setting.UpdatedAt, + }, nil +} + +func (p *OutletSettingProcessorImpl) GetPrinterSettings(ctx context.Context, outletID uuid.UUID) (*models.OutletPrinterSettings, error) { + // Check if outlet exists + outlet, err := p.outletRepo.GetByID(ctx, outletID) + if err != nil { + return nil, fmt.Errorf("outlet not found: %w", err) + } + + // Get printer settings from database + settings, err := p.outletSettingRepo.GetPrinterSettingsByOutletID(ctx, outletID) + if err != nil { + return nil, fmt.Errorf("failed to get printer settings: %w", err) + } + + // Build printer settings with defaults + printerSettings := &models.OutletPrinterSettings{ + OutletName: p.getSettingValue(settings, constants.PRINTER_OUTLET_NAME, outlet.Name), + Address: p.getSettingValue(settings, constants.PRINTER_ADDRESS, ""), + PhoneNumber: p.getSettingValue(settings, constants.PRINTER_PHONE_NUMBER, ""), + PaperSize: p.getSettingValue(settings, constants.PRINTER_PAPER_SIZE, constants.DEFAULT_PAPER_SIZE), + Footer: p.getSettingValue(settings, constants.PRINTER_FOOTER, constants.DEFAULT_FOOTER), + FooterHashtag: p.getSettingValue(settings, constants.PRINTER_FOOTER_HASHTAG, constants.DEFAULT_FOOTER_HASHTAG), + } + + return printerSettings, nil +} + +func (p *OutletSettingProcessorImpl) UpdatePrinterSettings(ctx context.Context, outletID uuid.UUID, req *models.UpdateOutletPrinterSettingsRequest) (*models.OutletPrinterSettings, error) { + // Check if outlet exists + _, err := p.outletRepo.GetByID(ctx, outletID) + if err != nil { + return nil, fmt.Errorf("outlet not found: %w", err) + } + + // Update each setting if provided + if req.OutletName != nil { + err = p.upsertSetting(ctx, outletID, constants.PRINTER_OUTLET_NAME, *req.OutletName) + if err != nil { + return nil, fmt.Errorf("failed to update outlet name: %w", err) + } + } + + if req.Address != nil { + err = p.upsertSetting(ctx, outletID, constants.PRINTER_ADDRESS, *req.Address) + if err != nil { + return nil, fmt.Errorf("failed to update address: %w", err) + } + } + + if req.PhoneNumber != nil { + err = p.upsertSetting(ctx, outletID, constants.PRINTER_PHONE_NUMBER, *req.PhoneNumber) + if err != nil { + return nil, fmt.Errorf("failed to update phone number: %w", err) + } + } + + if req.PaperSize != nil { + err = p.upsertSetting(ctx, outletID, constants.PRINTER_PAPER_SIZE, *req.PaperSize) + if err != nil { + return nil, fmt.Errorf("failed to update paper size: %w", err) + } + } + + if req.Footer != nil { + err = p.upsertSetting(ctx, outletID, constants.PRINTER_FOOTER, *req.Footer) + if err != nil { + return nil, fmt.Errorf("failed to update footer: %w", err) + } + } + + if req.FooterHashtag != nil { + err = p.upsertSetting(ctx, outletID, constants.PRINTER_FOOTER_HASHTAG, *req.FooterHashtag) + if err != nil { + return nil, fmt.Errorf("failed to update footer hashtag: %w", err) + } + } + + // Return updated settings + return p.GetPrinterSettings(ctx, outletID) +} + +func (p *OutletSettingProcessorImpl) DeleteSetting(ctx context.Context, outletID uuid.UUID, key string) error { + // Check if outlet exists + _, err := p.outletRepo.GetByID(ctx, outletID) + if err != nil { + return fmt.Errorf("outlet not found: %w", err) + } + + err = p.outletSettingRepo.DeleteByOutletIDAndKey(ctx, outletID, key) + if err != nil { + return fmt.Errorf("failed to delete setting: %w", err) + } + + return nil +} + +func (p *OutletSettingProcessorImpl) getSettingValue(settings map[string]string, key, defaultValue string) string { + if value, exists := settings[key]; exists { + return value + } + return defaultValue +} + +func (p *OutletSettingProcessorImpl) upsertSetting(ctx context.Context, outletID uuid.UUID, key, value string) error { + setting, err := p.outletSettingRepo.GetByOutletIDAndKey(ctx, outletID, key) + if err != nil { + // Setting doesn't exist, create new one + setting = &entities.OutletSetting{ + OutletID: outletID, + Key: key, + Value: value, + } + return p.outletSettingRepo.Create(ctx, setting) + } + + // Setting exists, update it + setting.Value = value + return p.outletSettingRepo.Update(ctx, setting) +} diff --git a/internal/processor/payment_method_processor.go b/internal/processor/payment_method_processor.go new file mode 100644 index 0000000..cac2996 --- /dev/null +++ b/internal/processor/payment_method_processor.go @@ -0,0 +1,190 @@ +package processor + +import ( + "context" + "fmt" + + "apskel-pos-be/internal/mappers" + "apskel-pos-be/internal/models" + "apskel-pos-be/internal/repository" + + "github.com/google/uuid" +) + +type PaymentMethodProcessor interface { + CreatePaymentMethod(ctx context.Context, req *models.CreatePaymentMethodRequest) (*models.PaymentMethodResponse, error) + GetPaymentMethodByID(ctx context.Context, id uuid.UUID) (*models.PaymentMethodResponse, error) + ListPaymentMethods(ctx context.Context, req *models.ListPaymentMethodsRequest) (*models.ListPaymentMethodsResponse, error) + UpdatePaymentMethod(ctx context.Context, id uuid.UUID, req *models.UpdatePaymentMethodRequest) (*models.PaymentMethodResponse, error) + DeletePaymentMethod(ctx context.Context, id uuid.UUID) error + GetActivePaymentMethodsByOrganization(ctx context.Context, organizationID uuid.UUID) ([]models.PaymentMethodResponse, error) +} + +type PaymentMethodProcessorImpl struct { + paymentMethodRepo repository.PaymentMethodRepository +} + +func NewPaymentMethodProcessorImpl(paymentMethodRepo repository.PaymentMethodRepository) *PaymentMethodProcessorImpl { + return &PaymentMethodProcessorImpl{ + paymentMethodRepo: paymentMethodRepo, + } +} + +func (p *PaymentMethodProcessorImpl) CreatePaymentMethod(ctx context.Context, req *models.CreatePaymentMethodRequest) (*models.PaymentMethodResponse, error) { + // Check if payment method with same name already exists + exists, err := p.paymentMethodRepo.ExistsByName(ctx, req.OrganizationID, req.Name, nil) + if err != nil { + return nil, fmt.Errorf("failed to check payment method name uniqueness: %w", err) + } + if exists { + return nil, fmt.Errorf("payment method with name '%s' already exists for this organization", req.Name) + } + + // Map request to entity + paymentMethodEntity := mappers.CreatePaymentMethodRequestToEntity(req) + + // Create payment method + if err := p.paymentMethodRepo.Create(ctx, paymentMethodEntity); err != nil { + return nil, fmt.Errorf("failed to create payment method: %w", err) + } + + // Get created payment method + createdPaymentMethod, err := p.paymentMethodRepo.GetByID(ctx, paymentMethodEntity.ID) + if err != nil { + return nil, fmt.Errorf("failed to retrieve created payment method: %w", err) + } + + // Map entity to response + response := mappers.PaymentMethodEntityToResponse(createdPaymentMethod) + return response, nil +} + +func (p *PaymentMethodProcessorImpl) GetPaymentMethodByID(ctx context.Context, id uuid.UUID) (*models.PaymentMethodResponse, error) { + paymentMethod, err := p.paymentMethodRepo.GetByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("payment method not found: %w", err) + } + + response := mappers.PaymentMethodEntityToResponse(paymentMethod) + return response, nil +} + +func (p *PaymentMethodProcessorImpl) ListPaymentMethods(ctx context.Context, req *models.ListPaymentMethodsRequest) (*models.ListPaymentMethodsResponse, error) { + // Build filters + filters := make(map[string]interface{}) + if req.OrganizationID != nil { + filters["organization_id"] = *req.OrganizationID + } + if req.Type != nil { + filters["type"] = string(*req.Type) + } + if req.IsActive != nil { + filters["is_active"] = *req.IsActive + } + if req.Search != "" { + filters["search"] = req.Search + } + + // Calculate offset + offset := (req.Page - 1) * req.Limit + + // Get payment methods + paymentMethods, total, err := p.paymentMethodRepo.List(ctx, filters, req.Limit, offset) + if err != nil { + return nil, fmt.Errorf("failed to list payment methods: %w", err) + } + + // Convert to responses + paymentMethodResponses := make([]models.PaymentMethodResponse, len(paymentMethods)) + for i, paymentMethod := range paymentMethods { + response := mappers.PaymentMethodEntityToResponse(paymentMethod) + if response != nil { + paymentMethodResponses[i] = *response + } + } + + // Calculate total pages + totalPages := int(total) / req.Limit + if int(total)%req.Limit > 0 { + totalPages++ + } + + return &models.ListPaymentMethodsResponse{ + PaymentMethods: paymentMethodResponses, + TotalCount: int(total), + Page: req.Page, + Limit: req.Limit, + TotalPages: totalPages, + }, nil +} + +func (p *PaymentMethodProcessorImpl) UpdatePaymentMethod(ctx context.Context, id uuid.UUID, req *models.UpdatePaymentMethodRequest) (*models.PaymentMethodResponse, error) { + // Get existing payment method + existingPaymentMethod, err := p.paymentMethodRepo.GetByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("payment method not found: %w", err) + } + + // Check name uniqueness if name is being updated + if req.Name != nil && *req.Name != existingPaymentMethod.Name { + exists, err := p.paymentMethodRepo.ExistsByName(ctx, existingPaymentMethod.OrganizationID, *req.Name, &id) + if err != nil { + return nil, fmt.Errorf("failed to check payment method name uniqueness: %w", err) + } + if exists { + return nil, fmt.Errorf("payment method with name '%s' already exists for this organization", *req.Name) + } + } + + // Apply updates + mappers.UpdatePaymentMethodEntityFromRequest(existingPaymentMethod, req) + + // Update payment method + if err := p.paymentMethodRepo.Update(ctx, existingPaymentMethod); err != nil { + return nil, fmt.Errorf("failed to update payment method: %w", err) + } + + // Get updated payment method + updatedPaymentMethod, err := p.paymentMethodRepo.GetByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("failed to retrieve updated payment method: %w", err) + } + + response := mappers.PaymentMethodEntityToResponse(updatedPaymentMethod) + return response, nil +} + +func (p *PaymentMethodProcessorImpl) DeletePaymentMethod(ctx context.Context, id uuid.UUID) error { + // Check if payment method exists + _, err := p.paymentMethodRepo.GetByID(ctx, id) + if err != nil { + return fmt.Errorf("payment method not found: %w", err) + } + + // TODO: Check if payment method is being used in any payments + // For now, allow deletion + + // Delete payment method + if err := p.paymentMethodRepo.Delete(ctx, id); err != nil { + return fmt.Errorf("failed to delete payment method: %w", err) + } + + return nil +} + +func (p *PaymentMethodProcessorImpl) GetActivePaymentMethodsByOrganization(ctx context.Context, organizationID uuid.UUID) ([]models.PaymentMethodResponse, error) { + paymentMethods, err := p.paymentMethodRepo.GetActiveByOrganizationID(ctx, organizationID) + if err != nil { + return nil, fmt.Errorf("failed to get active payment methods: %w", err) + } + + responses := make([]models.PaymentMethodResponse, len(paymentMethods)) + for i, paymentMethod := range paymentMethods { + response := mappers.PaymentMethodEntityToResponse(paymentMethod) + if response != nil { + responses[i] = *response + } + } + + return responses, nil +} diff --git a/internal/processor/product_processor.go b/internal/processor/product_processor.go new file mode 100644 index 0000000..3a28482 --- /dev/null +++ b/internal/processor/product_processor.go @@ -0,0 +1,307 @@ +package processor + +import ( + "context" + "fmt" + + "apskel-pos-be/internal/entities" + "apskel-pos-be/internal/mappers" + "apskel-pos-be/internal/models" + "apskel-pos-be/internal/repository" + + "github.com/google/uuid" +) + +type ProductProcessor interface { + CreateProduct(ctx context.Context, req *models.CreateProductRequest) (*models.ProductResponse, error) + UpdateProduct(ctx context.Context, id uuid.UUID, req *models.UpdateProductRequest) (*models.ProductResponse, error) + DeleteProduct(ctx context.Context, id uuid.UUID) error + GetProductByID(ctx context.Context, id uuid.UUID) (*models.ProductResponse, error) + ListProducts(ctx context.Context, filters map[string]interface{}, page, limit int) ([]models.ProductResponse, int, error) +} + +type ProductRepository interface { + Create(ctx context.Context, product *entities.Product) error + GetByID(ctx context.Context, id uuid.UUID) (*entities.Product, error) + GetWithCategory(ctx context.Context, id uuid.UUID) (*entities.Product, error) + GetWithRelations(ctx context.Context, id uuid.UUID) (*entities.Product, error) + GetByOrganization(ctx context.Context, organizationID uuid.UUID) ([]*entities.Product, error) + GetByCategory(ctx context.Context, categoryID uuid.UUID) ([]*entities.Product, error) + GetByBusinessType(ctx context.Context, businessType string) ([]*entities.Product, error) + GetActiveByCategoryID(ctx context.Context, categoryID uuid.UUID) ([]*entities.Product, error) + Update(ctx context.Context, product *entities.Product) error + Delete(ctx context.Context, id uuid.UUID) error + List(ctx context.Context, filters map[string]interface{}, limit, offset int) ([]*entities.Product, int64, error) + Count(ctx context.Context, filters map[string]interface{}) (int64, error) + GetBySKU(ctx context.Context, organizationID uuid.UUID, sku string) (*entities.Product, error) + ExistsBySKU(ctx context.Context, organizationID uuid.UUID, sku string, excludeID *uuid.UUID) (bool, error) + GetByName(ctx context.Context, organizationID uuid.UUID, name string) (*entities.Product, error) + ExistsByName(ctx context.Context, organizationID uuid.UUID, name string, excludeID *uuid.UUID) (bool, error) + UpdateActiveStatus(ctx context.Context, id uuid.UUID, isActive bool) error + GetLowCostProducts(ctx context.Context, organizationID uuid.UUID, maxCost float64) ([]*entities.Product, error) +} + +type ProductProcessorImpl struct { + productRepo ProductRepository + categoryRepo CategoryRepository + productVariantRepo repository.ProductVariantRepository + inventoryRepo InventoryRepository + outletRepo OutletRepository +} + +func NewProductProcessorImpl(productRepo ProductRepository, categoryRepo CategoryRepository, productVariantRepo repository.ProductVariantRepository, inventoryRepo InventoryRepository, outletRepo OutletRepository) *ProductProcessorImpl { + return &ProductProcessorImpl{ + productRepo: productRepo, + categoryRepo: categoryRepo, + productVariantRepo: productVariantRepo, + inventoryRepo: inventoryRepo, + outletRepo: outletRepo, + } +} + +func (p *ProductProcessorImpl) CreateProduct(ctx context.Context, req *models.CreateProductRequest) (*models.ProductResponse, error) { + _, err := p.categoryRepo.GetByID(ctx, req.CategoryID) + if err != nil { + return nil, fmt.Errorf("invalid category: %w", err) + } + + if req.SKU != nil && *req.SKU != "" { + exists, err := p.productRepo.ExistsBySKU(ctx, req.OrganizationID, *req.SKU, nil) + if err != nil { + return nil, fmt.Errorf("failed to check SKU uniqueness: %w", err) + } + if exists { + return nil, fmt.Errorf("product with SKU '%s' already exists for this organization", *req.SKU) + } + } + + exists, err := p.productRepo.ExistsByName(ctx, req.OrganizationID, req.Name, nil) + if err != nil { + return nil, fmt.Errorf("failed to check product name uniqueness: %w", err) + } + if exists { + return nil, fmt.Errorf("product with name '%s' already exists for this organization", req.Name) + } + + productEntity := mappers.CreateProductRequestToEntity(req) + + if err := p.productRepo.Create(ctx, productEntity); err != nil { + return nil, fmt.Errorf("failed to create product: %w", err) + } + + // Create variants if provided + if req.Variants != nil && len(req.Variants) > 0 { + for _, variantReq := range req.Variants { + // Set the product ID for the variant + variantReq.ProductID = productEntity.ID + + // Check variant name uniqueness within the same product + exists, err := p.productVariantRepo.ExistsByName(ctx, productEntity.ID, variantReq.Name, nil) + if err != nil { + return nil, fmt.Errorf("failed to check variant name uniqueness: %w", err) + } + if exists { + return nil, fmt.Errorf("variant with name '%s' already exists for this product", variantReq.Name) + } + + variantEntity := mappers.CreateProductVariantRequestToEntity(&variantReq) + if err := p.productVariantRepo.Create(ctx, variantEntity); err != nil { + return nil, fmt.Errorf("failed to create product variant: %w", err) + } + } + } + + // Create inventory records for all outlets if requested + if req.CreateInventory { + if err := p.createInventoryForAllOutlets(ctx, productEntity.ID, req.OrganizationID, req.InitialStock, req.ReorderLevel); err != nil { + return nil, fmt.Errorf("failed to create inventory records: %w", err) + } + } + + productWithCategory, err := p.productRepo.GetWithCategory(ctx, productEntity.ID) + if err != nil { + return nil, fmt.Errorf("failed to retrieve created product: %w", err) + } + + response := mappers.ProductEntityToResponse(productWithCategory) + return response, nil +} + +func (p *ProductProcessorImpl) UpdateProduct(ctx context.Context, id uuid.UUID, req *models.UpdateProductRequest) (*models.ProductResponse, error) { + existingProduct, err := p.productRepo.GetByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("product not found: %w", err) + } + + if req.CategoryID != nil { + _, err := p.categoryRepo.GetByID(ctx, *req.CategoryID) + if err != nil { + return nil, fmt.Errorf("invalid category: %w", err) + } + } + + if req.SKU != nil && *req.SKU != "" { + currentSKU := "" + if existingProduct.SKU != nil { + currentSKU = *existingProduct.SKU + } + if *req.SKU != currentSKU { + exists, err := p.productRepo.ExistsBySKU(ctx, existingProduct.OrganizationID, *req.SKU, &id) + if err != nil { + return nil, fmt.Errorf("failed to check SKU uniqueness: %w", err) + } + if exists { + return nil, fmt.Errorf("product with SKU '%s' already exists for this organization", *req.SKU) + } + } + } + + if req.Name != nil && *req.Name != existingProduct.Name { + exists, err := p.productRepo.ExistsByName(ctx, existingProduct.OrganizationID, *req.Name, &id) + if err != nil { + return nil, fmt.Errorf("failed to check product name uniqueness: %w", err) + } + if exists { + return nil, fmt.Errorf("product with name '%s' already exists for this organization", *req.Name) + } + } + + mappers.UpdateProductEntityFromRequest(existingProduct, req) + + if err := p.productRepo.Update(ctx, existingProduct); err != nil { + return nil, fmt.Errorf("failed to update product: %w", err) + } + + // Update reorder level for all existing inventory records if provided + if req.ReorderLevel != nil { + if err := p.updateReorderLevelForAllOutlets(ctx, id, *req.ReorderLevel); err != nil { + return nil, fmt.Errorf("failed to update reorder levels: %w", err) + } + } + + productWithCategory, err := p.productRepo.GetWithCategory(ctx, id) + if err != nil { + return nil, fmt.Errorf("failed to retrieve updated product: %w", err) + } + + response := mappers.ProductEntityToResponse(productWithCategory) + return response, nil +} + +func (p *ProductProcessorImpl) DeleteProduct(ctx context.Context, id uuid.UUID) error { + _, err := p.productRepo.GetByID(ctx, id) + if err != nil { + return fmt.Errorf("product not found: %w", err) + } + + productWithRelations, err := p.productRepo.GetWithRelations(ctx, id) + if err != nil { + return fmt.Errorf("failed to check product relations: %w", err) + } + + if len(productWithRelations.Inventory) > 0 { + return fmt.Errorf("cannot delete product: it has inventory records associated with it") + } + + if len(productWithRelations.OrderItems) > 0 { + return fmt.Errorf("cannot delete product: it has order items associated with it") + } + + if err := p.productRepo.Delete(ctx, id); err != nil { + return fmt.Errorf("failed to delete product: %w", err) + } + + return nil +} + +func (p *ProductProcessorImpl) GetProductByID(ctx context.Context, id uuid.UUID) (*models.ProductResponse, error) { + productEntity, err := p.productRepo.GetWithCategory(ctx, id) + if err != nil { + return nil, fmt.Errorf("product not found: %w", err) + } + + response := mappers.ProductEntityToResponse(productEntity) + return response, nil +} + +func (p *ProductProcessorImpl) ListProducts(ctx context.Context, filters map[string]interface{}, page, limit int) ([]models.ProductResponse, int, error) { + offset := (page - 1) * limit + + productEntities, total, err := p.productRepo.List(ctx, filters, limit, offset) + if err != nil { + return nil, 0, fmt.Errorf("failed to list products: %w", err) + } + + responses := make([]models.ProductResponse, len(productEntities)) + for i, entity := range productEntities { + response := mappers.ProductEntityToResponse(entity) + if response != nil { + responses[i] = *response + } + } + + return responses, int(total), nil +} + +// Helper methods for inventory management + +// createInventoryForAllOutlets creates inventory records for all outlets of an organization +func (p *ProductProcessorImpl) createInventoryForAllOutlets(ctx context.Context, productID, organizationID uuid.UUID, initialStock, reorderLevel *int) error { + // Get all outlets for the organization + outlets, err := p.outletRepo.GetByOrganizationID(ctx, organizationID) + if err != nil { + return fmt.Errorf("failed to get outlets for organization: %w", err) + } + + if len(outlets) == 0 { + return fmt.Errorf("no outlets found for organization") + } + + // Prepare inventory items for bulk creation + var inventoryItems []*entities.Inventory + for _, outlet := range outlets { + quantity := 0 + if initialStock != nil { + quantity = *initialStock + } + + reorderLevelValue := 0 + if reorderLevel != nil { + reorderLevelValue = *reorderLevel + } + + inventoryItem := &entities.Inventory{ + OutletID: outlet.ID, + ProductID: productID, + Quantity: quantity, + ReorderLevel: reorderLevelValue, + } + inventoryItems = append(inventoryItems, inventoryItem) + } + + // Bulk create inventory records + if err := p.inventoryRepo.BulkCreate(ctx, inventoryItems); err != nil { + return fmt.Errorf("failed to bulk create inventory records: %w", err) + } + + return nil +} + +// updateReorderLevelForAllOutlets updates the reorder level for all inventory records of a product +func (p *ProductProcessorImpl) updateReorderLevelForAllOutlets(ctx context.Context, productID uuid.UUID, reorderLevel int) error { + // Get all inventory records for the product + inventoryRecords, err := p.inventoryRepo.GetByProduct(ctx, productID) + if err != nil { + return fmt.Errorf("failed to get inventory records for product: %w", err) + } + + // Update reorder level for each inventory record + for _, inventory := range inventoryRecords { + inventory.ReorderLevel = reorderLevel + if err := p.inventoryRepo.Update(ctx, inventory); err != nil { + return fmt.Errorf("failed to update inventory reorder level: %w", err) + } + } + + return nil +} diff --git a/internal/processor/product_processor_test.go b/internal/processor/product_processor_test.go new file mode 100644 index 0000000..297e6c1 --- /dev/null +++ b/internal/processor/product_processor_test.go @@ -0,0 +1,180 @@ +package processor + +import ( + "context" + "testing" + + "apskel-pos-be/internal/models" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +// Mock repositories for testing +type MockProductRepository struct { + mock.Mock +} + +type MockCategoryRepository struct { + mock.Mock +} + +type MockProductVariantRepository struct { + mock.Mock +} + +type MockInventoryRepository struct { + mock.Mock +} + +type MockOutletRepository struct { + mock.Mock +} + +// Test helper functions +func TestCreateProductWithInventory(t *testing.T) { + // This is a basic test structure - in a real implementation, + // you would use a proper testing framework with database mocks + + t.Run("should create product with inventory when create_inventory is true", func(t *testing.T) { + // Arrange + productRepo := &MockProductRepository{} + categoryRepo := &MockCategoryRepository{} + productVariantRepo := &MockProductVariantRepository{} + inventoryRepo := &MockInventoryRepository{} + outletRepo := &MockOutletRepository{} + + processor := NewProductProcessorImpl( + productRepo, + categoryRepo, + productVariantRepo, + inventoryRepo, + outletRepo, + ) + + req := &models.CreateProductRequest{ + OrganizationID: uuid.New(), + CategoryID: uuid.New(), + Name: "Test Product", + Price: 10.0, + Cost: 5.0, + InitialStock: &[]int{100}[0], + ReorderLevel: &[]int{20}[0], + CreateInventory: true, + } + + // Mock expectations + categoryRepo.On("GetByID", mock.Anything, req.CategoryID).Return(&models.Category{}, nil) + productRepo.On("ExistsBySKU", mock.Anything, req.OrganizationID, mock.Anything, mock.Anything).Return(false, nil) + productRepo.On("ExistsByName", mock.Anything, req.OrganizationID, req.Name, mock.Anything).Return(false, nil) + productRepo.On("Create", mock.Anything, mock.Anything).Return(nil) + productRepo.On("GetWithCategory", mock.Anything, mock.Anything).Return(&models.Product{}, nil) + + // Mock outlets + outlets := []*models.Outlet{ + {ID: uuid.New()}, + {ID: uuid.New()}, + } + outletRepo.On("GetByOrganizationID", mock.Anything, req.OrganizationID).Return(outlets, nil) + + // Mock inventory creation + inventoryRepo.On("BulkCreate", mock.Anything, mock.Anything).Return(nil) + + // Act + result, err := processor.CreateProduct(context.Background(), req) + + // Assert + assert.NoError(t, err) + assert.NotNil(t, result) + + // Verify that inventory was created + inventoryRepo.AssertCalled(t, "BulkCreate", mock.Anything, mock.Anything) + outletRepo.AssertCalled(t, "GetByOrganizationID", mock.Anything, req.OrganizationID) + }) + + t.Run("should not create inventory when create_inventory is false", func(t *testing.T) { + // Arrange + productRepo := &MockProductRepository{} + categoryRepo := &MockCategoryRepository{} + productVariantRepo := &MockProductVariantRepository{} + inventoryRepo := &MockInventoryRepository{} + outletRepo := &MockOutletRepository{} + + processor := NewProductProcessorImpl( + productRepo, + categoryRepo, + productVariantRepo, + inventoryRepo, + outletRepo, + ) + + req := &models.CreateProductRequest{ + OrganizationID: uuid.New(), + CategoryID: uuid.New(), + Name: "Test Product", + Price: 10.0, + Cost: 5.0, + CreateInventory: false, + } + + // Mock expectations + categoryRepo.On("GetByID", mock.Anything, req.CategoryID).Return(&models.Category{}, nil) + productRepo.On("ExistsBySKU", mock.Anything, req.OrganizationID, mock.Anything, mock.Anything).Return(false, nil) + productRepo.On("ExistsByName", mock.Anything, req.OrganizationID, req.Name, mock.Anything).Return(false, nil) + productRepo.On("Create", mock.Anything, mock.Anything).Return(nil) + productRepo.On("GetWithCategory", mock.Anything, mock.Anything).Return(&models.Product{}, nil) + + // Act + result, err := processor.CreateProduct(context.Background(), req) + + // Assert + assert.NoError(t, err) + assert.NotNil(t, result) + + // Verify that inventory was NOT created + inventoryRepo.AssertNotCalled(t, "BulkCreate", mock.Anything, mock.Anything) + outletRepo.AssertNotCalled(t, "GetByOrganizationID", mock.Anything, mock.Anything) + }) +} + +// Mock implementations (simplified for testing) +func (m *MockProductRepository) Create(ctx context.Context, product *models.Product) error { + args := m.Called(ctx, product) + return args.Error(0) +} + +func (m *MockProductRepository) GetByID(ctx context.Context, id uuid.UUID) (*models.Product, error) { + args := m.Called(ctx, id) + return args.Get(0).(*models.Product), args.Error(1) +} + +func (m *MockProductRepository) GetWithCategory(ctx context.Context, id uuid.UUID) (*models.Product, error) { + args := m.Called(ctx, id) + return args.Get(0).(*models.Product), args.Error(1) +} + +func (m *MockProductRepository) ExistsBySKU(ctx context.Context, organizationID uuid.UUID, sku string, excludeID *uuid.UUID) (bool, error) { + args := m.Called(ctx, organizationID, sku, excludeID) + return args.Bool(0), args.Error(1) +} + +func (m *MockProductRepository) ExistsByName(ctx context.Context, organizationID uuid.UUID, name string, excludeID *uuid.UUID) (bool, error) { + args := m.Called(ctx, organizationID, name, excludeID) + return args.Bool(0), args.Error(1) +} + +func (m *MockCategoryRepository) GetByID(ctx context.Context, id uuid.UUID) (*models.Category, error) { + args := m.Called(ctx, id) + return args.Get(0).(*models.Category), args.Error(1) +} + +func (m *MockInventoryRepository) BulkCreate(ctx context.Context, inventoryItems []*models.Inventory) error { + args := m.Called(ctx, inventoryItems) + return args.Error(0) +} + +func (m *MockOutletRepository) GetByOrganizationID(ctx context.Context, organizationID uuid.UUID) ([]*models.Outlet, error) { + args := m.Called(ctx, organizationID) + return args.Get(0).([]*models.Outlet), args.Error(1) +} diff --git a/internal/processor/product_variant_processor.go b/internal/processor/product_variant_processor.go new file mode 100644 index 0000000..ef2436b --- /dev/null +++ b/internal/processor/product_variant_processor.go @@ -0,0 +1,138 @@ +package processor + +import ( + "context" + "fmt" + + "apskel-pos-be/internal/mappers" + "apskel-pos-be/internal/models" + "apskel-pos-be/internal/repository" + + "github.com/google/uuid" +) + +type ProductVariantProcessor interface { + CreateProductVariant(ctx context.Context, req *models.CreateProductVariantRequest) (*models.ProductVariantResponse, error) + UpdateProductVariant(ctx context.Context, id uuid.UUID, req *models.UpdateProductVariantRequest) (*models.ProductVariantResponse, error) + DeleteProductVariant(ctx context.Context, id uuid.UUID) error + GetProductVariantByID(ctx context.Context, id uuid.UUID) (*models.ProductVariantResponse, error) + GetProductVariantsByProductID(ctx context.Context, productID uuid.UUID) ([]models.ProductVariantResponse, error) +} + +type ProductVariantProcessorImpl struct { + productVariantRepo repository.ProductVariantRepository + productRepo ProductRepository +} + +func NewProductVariantProcessorImpl( + productVariantRepo repository.ProductVariantRepository, + productRepo ProductRepository, +) *ProductVariantProcessorImpl { + return &ProductVariantProcessorImpl{ + productVariantRepo: productVariantRepo, + productRepo: productRepo, + } +} + +func (p *ProductVariantProcessorImpl) CreateProductVariant(ctx context.Context, req *models.CreateProductVariantRequest) (*models.ProductVariantResponse, error) { + // Validate product exists + _, err := p.productRepo.GetByID(ctx, req.ProductID) + if err != nil { + return nil, fmt.Errorf("invalid product: %w", err) + } + + // Check name uniqueness within the same product + exists, err := p.productVariantRepo.ExistsByName(ctx, req.ProductID, req.Name, nil) + if err != nil { + return nil, fmt.Errorf("failed to check variant name uniqueness: %w", err) + } + if exists { + return nil, fmt.Errorf("variant with name '%s' already exists for this product", req.Name) + } + + // Map request to entity + variantEntity := mappers.CreateProductVariantRequestToEntity(req) + + // Create variant + if err := p.productVariantRepo.Create(ctx, variantEntity); err != nil { + return nil, fmt.Errorf("failed to create product variant: %w", err) + } + + // Map entity to response model + response := mappers.ProductVariantEntityToResponse(variantEntity) + return response, nil +} + +func (p *ProductVariantProcessorImpl) UpdateProductVariant(ctx context.Context, id uuid.UUID, req *models.UpdateProductVariantRequest) (*models.ProductVariantResponse, error) { + // Get existing variant + existingVariant, err := p.productVariantRepo.GetByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("product variant not found: %w", err) + } + + // Check name uniqueness if being updated + if req.Name != nil && *req.Name != existingVariant.Name { + exists, err := p.productVariantRepo.ExistsByName(ctx, existingVariant.ProductID, *req.Name, &id) + if err != nil { + return nil, fmt.Errorf("failed to check variant name uniqueness: %w", err) + } + if exists { + return nil, fmt.Errorf("variant with name '%s' already exists for this product", *req.Name) + } + } + + // Apply updates to entity + mappers.UpdateProductVariantEntityFromRequest(existingVariant, req) + + // Update variant + if err := p.productVariantRepo.Update(ctx, existingVariant); err != nil { + return nil, fmt.Errorf("failed to update product variant: %w", err) + } + + // Map entity to response model + response := mappers.ProductVariantEntityToResponse(existingVariant) + return response, nil +} + +func (p *ProductVariantProcessorImpl) DeleteProductVariant(ctx context.Context, id uuid.UUID) error { + // Check if variant exists + _, err := p.productVariantRepo.GetByID(ctx, id) + if err != nil { + return fmt.Errorf("product variant not found: %w", err) + } + + // TODO: Check if variant is used in any order items before deletion + // This would require checking the order_items table + + // Delete variant + if err := p.productVariantRepo.Delete(ctx, id); err != nil { + return fmt.Errorf("failed to delete product variant: %w", err) + } + + return nil +} + +func (p *ProductVariantProcessorImpl) GetProductVariantByID(ctx context.Context, id uuid.UUID) (*models.ProductVariantResponse, error) { + variant, err := p.productVariantRepo.GetByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("product variant not found: %w", err) + } + + response := mappers.ProductVariantEntityToResponse(variant) + return response, nil +} + +func (p *ProductVariantProcessorImpl) GetProductVariantsByProductID(ctx context.Context, productID uuid.UUID) ([]models.ProductVariantResponse, error) { + variants, err := p.productVariantRepo.GetByProductID(ctx, productID) + if err != nil { + return nil, fmt.Errorf("failed to get product variants: %w", err) + } + + responses := make([]models.ProductVariantResponse, len(variants)) + for i, variant := range variants { + response := mappers.ProductVariantEntityToResponse(variant) + responses[i] = *response + } + + return responses, nil +} diff --git a/internal/processor/user_processor.go b/internal/processor/user_processor.go new file mode 100644 index 0000000..e47cd62 --- /dev/null +++ b/internal/processor/user_processor.go @@ -0,0 +1,226 @@ +package processor + +import ( + "context" + "fmt" + + "golang.org/x/crypto/bcrypt" + + "apskel-pos-be/internal/entities" + "apskel-pos-be/internal/mappers" + "apskel-pos-be/internal/models" + + "github.com/google/uuid" +) + +type UserProcessorImpl struct { + userRepo UserRepository + organizationRepo OrganizationRepository + outletRepo OutletRepository +} + +func NewUserProcessor( + userRepo UserRepository, + organizationRepo OrganizationRepository, + outletRepo OutletRepository, +) *UserProcessorImpl { + return &UserProcessorImpl{ + userRepo: userRepo, + organizationRepo: organizationRepo, + outletRepo: outletRepo, + } +} + +func (p *UserProcessorImpl) CreateUser(ctx context.Context, req *models.CreateUserRequest) (*models.UserResponse, error) { + _, err := p.organizationRepo.GetByID(ctx, req.OrganizationID) + if err != nil { + return nil, fmt.Errorf("organization not found: %w", err) + } + + if req.OutletID != nil { + _, err := p.outletRepo.GetByID(ctx, *req.OutletID) + if err != nil { + return nil, fmt.Errorf("outlet not found: %w", err) + } + } + + existingUser, err := p.userRepo.GetByEmail(ctx, req.Email) + if err == nil && existingUser != nil { + return nil, fmt.Errorf("user with email %s already exists", req.Email) + } + + passwordHash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) + if err != nil { + return nil, fmt.Errorf("failed to hash password: %w", err) + } + + userEntity := mappers.UserCreateRequestToEntity(req, string(passwordHash)) + + err = p.userRepo.Create(ctx, userEntity) + if err != nil { + return nil, fmt.Errorf("failed to create user: %w", err) + } + + return mappers.UserEntityToResponse(userEntity), nil +} + +func (p *UserProcessorImpl) UpdateUser(ctx context.Context, id uuid.UUID, req *models.UpdateUserRequest) (*models.UserResponse, error) { + existingUser, err := p.userRepo.GetByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("user not found: %w", err) + } + + if req.Email != nil && *req.Email != existingUser.Email { + existingUserByEmail, err := p.userRepo.GetByEmail(ctx, *req.Email) + if err == nil && existingUserByEmail != nil && existingUserByEmail.ID != id { + return nil, fmt.Errorf("user with email %s already exists", *req.Email) + } + } + + if req.Name != nil { + existingUser.Name = *req.Name + } + if req.Email != nil { + existingUser.Email = *req.Email + } + if req.Role != nil { + existingUser.Role = entities.UserRole(*req.Role) + } + if req.OutletID != nil { + existingUser.OutletID = req.OutletID + } + if req.IsActive != nil { + existingUser.IsActive = *req.IsActive + } + if req.Permissions != nil { + existingUser.Permissions = entities.Permissions(*req.Permissions) + } + + err = p.userRepo.Update(ctx, existingUser) + if err != nil { + return nil, fmt.Errorf("failed to update user: %w", err) + } + + return mappers.UserEntityToResponse(existingUser), nil +} + +func (p *UserProcessorImpl) DeleteUser(ctx context.Context, id uuid.UUID) error { + _, err := p.userRepo.GetByID(ctx, id) + if err != nil { + return fmt.Errorf("user not found: %w", err) + } + + err = p.userRepo.Delete(ctx, id) + if err != nil { + return fmt.Errorf("failed to delete user: %w", err) + } + + return nil +} + +func (p *UserProcessorImpl) GetUserByID(ctx context.Context, id uuid.UUID) (*models.UserResponse, error) { + user, err := p.userRepo.GetByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("user not found: %w", err) + } + + return mappers.UserEntityToResponse(user), nil +} + +func (p *UserProcessorImpl) GetUserByEmail(ctx context.Context, email string) (*models.UserResponse, error) { + user, err := p.userRepo.GetByEmail(ctx, email) + if err != nil { + return nil, fmt.Errorf("user not found: %w", err) + } + + return mappers.UserEntityToResponse(user), nil +} + +func (p *UserProcessorImpl) ListUsers(ctx context.Context, organizationID uuid.UUID, page, limit int) ([]models.UserResponse, int, error) { + _, err := p.organizationRepo.GetByID(ctx, organizationID) + if err != nil { + return nil, 0, fmt.Errorf("organization not found: %w", err) + } + + offset := (page - 1) * limit + + filters := map[string]interface{}{ + "organization_id": organizationID, + } + + users, totalCount, err := p.userRepo.List(ctx, filters, limit, offset) + if err != nil { + return nil, 0, fmt.Errorf("failed to get users: %w", err) + } + + responses := make([]models.UserResponse, len(users)) + for i, user := range users { + response := mappers.UserEntityToResponse(user) + if response != nil { + responses[i] = *response + } + } + + return responses, int(totalCount), nil +} + +func (p *UserProcessorImpl) GetUserEntityByEmail(ctx context.Context, email string) (*entities.User, error) { + user, err := p.userRepo.GetByEmail(ctx, email) + if err != nil { + return nil, fmt.Errorf("user not found: %w", err) + } + + return user, nil +} + +func (p *UserProcessorImpl) ChangePassword(ctx context.Context, userID uuid.UUID, req *models.ChangePasswordRequest) error { + user, err := p.userRepo.GetByID(ctx, userID) + if err != nil { + return fmt.Errorf("user not found: %w", err) + } + + err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.CurrentPassword)) + if err != nil { + return fmt.Errorf("current password is incorrect") + } + + newPasswordHash, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost) + if err != nil { + return fmt.Errorf("failed to hash new password: %w", err) + } + + err = p.userRepo.UpdatePassword(ctx, userID, string(newPasswordHash)) + if err != nil { + return fmt.Errorf("failed to update password: %w", err) + } + + return nil +} + +func (p *UserProcessorImpl) ActivateUser(ctx context.Context, userID uuid.UUID) error { + _, err := p.userRepo.GetByID(ctx, userID) + if err != nil { + return fmt.Errorf("user not found: %w", err) + } + + err = p.userRepo.UpdateActiveStatus(ctx, userID, true) + if err != nil { + return fmt.Errorf("failed to activate user: %w", err) + } + + return nil +} + +func (p *UserProcessorImpl) DeactivateUser(ctx context.Context, userID uuid.UUID) error { + _, err := p.userRepo.GetByID(ctx, userID) + if err != nil { + return fmt.Errorf("user not found: %w", err) + } + + err = p.userRepo.UpdateActiveStatus(ctx, userID, false) + if err != nil { + return fmt.Errorf("failed to deactivate user: %w", err) + } + + return nil +} diff --git a/internal/processor/user_repository.go b/internal/processor/user_repository.go new file mode 100644 index 0000000..6b71b69 --- /dev/null +++ b/internal/processor/user_repository.go @@ -0,0 +1,22 @@ +package processor + +import ( + "apskel-pos-be/internal/entities" + "context" + "github.com/google/uuid" +) + +type UserRepository interface { + Create(ctx context.Context, user *entities.User) error + GetByID(ctx context.Context, id uuid.UUID) (*entities.User, error) + GetByEmail(ctx context.Context, email string) (*entities.User, error) + GetByOrganizationID(ctx context.Context, organizationID uuid.UUID) ([]*entities.User, error) + GetByRole(ctx context.Context, role entities.UserRole) ([]*entities.User, error) + GetActiveUsers(ctx context.Context, organizationID uuid.UUID) ([]*entities.User, error) + Update(ctx context.Context, user *entities.User) error + Delete(ctx context.Context, id uuid.UUID) error + UpdatePassword(ctx context.Context, id uuid.UUID, passwordHash string) error + UpdateActiveStatus(ctx context.Context, id uuid.UUID, isActive bool) error + List(ctx context.Context, filters map[string]interface{}, limit, offset int) ([]*entities.User, int64, error) + Count(ctx context.Context, filters map[string]interface{}) (int64, error) +} diff --git a/internal/repository/.DS_Store b/internal/repository/.DS_Store deleted file mode 100644 index 347dec1015b20fdcb3526db5df3e840eabee0784..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKOKyWO5Pi;@l1b1=u#+4`lOK0gbsTw`;#L`uXE(zZXSCSI&o99a zH4Pugu}6s}HqO1V#3!m)y`XQ63r>gK zJ6xt{voT-{^ck4yd{UEzC28@BbV!+KdubVY*DeSGIo0GjZ<#^)|5x-9H k8p4I&is>s`@e_v^>Qh+|vw)o 0 { - query = query.Limit(limit) - } - - if offset > 0 { - query = query.Offset(offset) - } - - if err := query.Find(&ordersDB).Error; err != nil { - return nil, errors.Wrap(err, "failed to find orders by partner ID") - } - - orders := make([]*entity.Order, 0, len(ordersDB)) - for _, orderDB := range ordersDB { - order := r.toDomainOrderModel(&orderDB) - - var orderItems []models.OrderItemDB - if err := r.db.Where("order_id = ?", orderDB.ID).Find(&orderItems).Error; err != nil { - return nil, errors.Wrap(err, "failed to find order items") - } - - order.OrderItems = make([]entity.OrderItem, 0, len(orderItems)) - - for _, itemDB := range orderItems { - item := r.toDomainOrderItemModel(&itemDB) - - orderItem := entity.OrderItem{ - ID: item.ID, - ItemID: item.ItemID, - Quantity: item.Quantity, - ItemName: item.ItemName, - } - - if itemDB.ItemID > 0 { - var product models.ProductDB - err := r.db.First(&product, itemDB.ItemID).Error - - if err == nil { - productDomain := r.toDomainProductModel(&product) - orderItem.Product = productDomain - } - } - - order.OrderItems = append(order.OrderItems, orderItem) - } - - orders = append(orders, order) - } - - return orders, nil -} - -func (r *inprogressOrderRepository) FindByIDAndPartnerID(ctx mycontext.Context, id int64, partnerID int64) (*entity.Order, error) { - var orderDB models.OrderDB - - if err := r.db.Preload("OrderItems").Where("id = ? AND partner_id = ?", id, partnerID).First(&orderDB).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, errors.New("order not found") - } - return nil, errors.Wrap(err, "failed to find order") - } - - order := r.toDomainOrderModel(&orderDB) - - for _, itemDB := range orderDB.OrderItems { - item := r.toDomainOrderItemModel(&itemDB) - order.OrderItems = append(order.OrderItems, *item) - } - - return order, nil -} - -func (r *inprogressOrderRepository) toOrderDBModel(order *entity.Order) models.OrderDB { - now := time2.Now() - return models.OrderDB{ - ID: order.ID, - PartnerID: order.PartnerID, - CustomerID: order.CustomerID, - CustomerName: order.CustomerName, - PaymentType: order.PaymentType, - PaymentProvider: order.PaymentProvider, - CreatedBy: order.CreatedBy, - CreatedAt: now, - UpdatedAt: now, - TableNumber: order.TableNumber, - OrderType: order.OrderType, - Status: order.Status, - Amount: order.Amount, - Total: order.Total, - Tax: order.Tax, - Source: order.Source, - } -} - -func (r *inprogressOrderRepository) toDomainOrderModel(dbModel *models.OrderDB) *entity.Order { - orderItems := make([]entity.OrderItem, 0, len(dbModel.OrderItems)) - for _, itemDB := range dbModel.OrderItems { - orderItems = append(orderItems, entity.OrderItem{ - ItemID: itemDB.ItemID, - ItemType: itemDB.ItemType, - ItemName: itemDB.ItemName, - Price: itemDB.Price, - Quantity: itemDB.Quantity, - Status: itemDB.Status, - CreatedBy: itemDB.CreatedBy, - CreatedAt: itemDB.CreatedAt, - Notes: itemDB.Notes, - }) - } - - return &entity.Order{ - ID: dbModel.ID, - PartnerID: dbModel.PartnerID, - CustomerID: dbModel.CustomerID, - InquiryID: dbModel.InquiryID, - Status: dbModel.Status, - Amount: dbModel.Amount, - Tax: dbModel.Tax, - Total: dbModel.Total, - PaymentType: dbModel.PaymentType, - Source: dbModel.Source, - CreatedBy: dbModel.CreatedBy, - CreatedAt: dbModel.CreatedAt, - UpdatedAt: dbModel.UpdatedAt, - OrderItems: orderItems, - CustomerName: dbModel.CustomerName, - TableNumber: dbModel.TableNumber, - OrderType: dbModel.OrderType, - PaymentProvider: dbModel.PaymentProvider, - } -} - -func (r *inprogressOrderRepository) toOrderItemDBModel(item *entity.OrderItem) models.OrderItemDB { - return models.OrderItemDB{ - ID: item.ID, - OrderID: item.OrderID, - ItemID: item.ItemID, - ItemType: item.ItemType, - ItemName: item.ItemName, - Price: item.Price, - Quantity: item.Quantity, - Status: item.Status, - CreatedBy: item.CreatedBy, - CreatedAt: item.CreatedAt, - Notes: item.Notes, - } -} - -func (r *inprogressOrderRepository) toDomainOrderItemModel(dbModel *models.OrderItemDB) *entity.OrderItem { - return &entity.OrderItem{ - ID: dbModel.ID, - OrderID: dbModel.OrderID, - ItemID: dbModel.ItemID, - ItemType: dbModel.ItemType, - Price: dbModel.Price, - Quantity: dbModel.Quantity, - Status: dbModel.Status, - CreatedBy: dbModel.CreatedBy, - CreatedAt: dbModel.CreatedAt, - ItemName: dbModel.ItemName, - Notes: dbModel.Notes, - Product: &entity.Product{ - ID: dbModel.ItemID, - Name: dbModel.ItemName, - }, - } -} - -func (r *inprogressOrderRepository) toDomainProductModel(productDB *models.ProductDB) *entity.Product { - if productDB == nil { - return nil - } - - return &entity.Product{ - ID: productDB.ID, - Name: productDB.Name, - Description: productDB.Description, - Price: productDB.Price, - CreatedAt: productDB.CreatedAt, - UpdatedAt: productDB.UpdatedAt, - Type: productDB.Type, - Image: productDB.Image, - } -} - -func (r *inprogressOrderRepository) UpdateOrderTotalsWithTx(ctx mycontext.Context, trx *gorm.DB, orderID int64, amount, tax, total float64) error { - now := time2.Now() - - result := trx.Model(&models.OrderDB{}). - Where("id = ?", orderID). - Updates(map[string]interface{}{ - "amount": amount, - "tax": tax, - "total": total, - "updated_at": now, - }) - - if result.Error != nil { - return errors.Wrap(result.Error, "failed to update order totals") - } - - if result.RowsAffected == 0 { - logger.ContextLogger(ctx).Warn("no order updated") - } - - return nil -} diff --git a/internal/repository/analytics_repository.go b/internal/repository/analytics_repository.go new file mode 100644 index 0000000..35ef086 --- /dev/null +++ b/internal/repository/analytics_repository.go @@ -0,0 +1,328 @@ +package repository + +import ( + "context" + "time" + + "apskel-pos-be/internal/entities" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type AnalyticsRepository interface { + GetPaymentMethodAnalytics(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time) ([]*entities.PaymentMethodAnalytics, error) + GetSalesAnalytics(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time, groupBy string) ([]*entities.SalesAnalytics, error) + GetProductAnalytics(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time, limit int) ([]*entities.ProductAnalytics, error) + GetDashboardOverview(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time) (*entities.DashboardOverview, error) + GetProfitLossAnalytics(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time, groupBy string) (*entities.ProfitLossAnalytics, error) +} + +type AnalyticsRepositoryImpl struct { + db *gorm.DB +} + +func NewAnalyticsRepositoryImpl(db *gorm.DB) *AnalyticsRepositoryImpl { + return &AnalyticsRepositoryImpl{ + db: db, + } +} + +func (r *AnalyticsRepositoryImpl) GetPaymentMethodAnalytics(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time) ([]*entities.PaymentMethodAnalytics, error) { + var results []*entities.PaymentMethodAnalytics + + query := r.db.WithContext(ctx). + Table("payments p"). + Select(` + pm.id as payment_method_id, + pm.name as payment_method_name, + pm.type as payment_method_type, + COALESCE(SUM(p.amount), 0) as total_amount, + COUNT(DISTINCT p.order_id) as order_count, + COUNT(p.id) as payment_count + `). + Joins("JOIN payment_methods pm ON p.payment_method_id = pm.id"). + Joins("JOIN orders o ON p.order_id = o.id"). + Where("o.organization_id = ?", organizationID). + Where("p.status = ?", entities.PaymentTransactionStatusCompleted). + Where("p.created_at >= ? AND p.created_at <= ?", dateFrom, dateTo) + + if outletID != nil { + query = query.Where("o.outlet_id = ?", *outletID) + } + + err := query. + Group("pm.id, pm.name, pm.type"). + Order("total_amount DESC"). + Scan(&results).Error + + return results, err +} + +func (r *AnalyticsRepositoryImpl) GetSalesAnalytics(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time, groupBy string) ([]*entities.SalesAnalytics, error) { + var results []*entities.SalesAnalytics + + var dateFormat string + switch groupBy { + case "hour": + dateFormat = "DATE_TRUNC('hour', o.created_at)" + case "week": + dateFormat = "DATE_TRUNC('week', o.created_at)" + case "month": + dateFormat = "DATE_TRUNC('month', o.created_at)" + default: + dateFormat = "DATE(o.created_at)" + } + + query := r.db.WithContext(ctx). + Table("orders o"). + Select(` + `+dateFormat+` as date, + COALESCE(SUM(o.total_amount), 0) as sales, + COUNT(o.id) as orders, + COALESCE(SUM(oi.quantity), 0) as items, + COALESCE(SUM(o.tax_amount), 0) as tax, + COALESCE(SUM(o.discount_amount), 0) as discount, + COALESCE(SUM(o.total_amount - o.tax_amount - o.discount_amount), 0) as net_sales + `). + Joins("LEFT JOIN order_items oi ON o.id = oi.order_id"). + Where("o.organization_id = ?", organizationID). + Where("o.is_void = ?", false). + Where("o.created_at >= ? AND o.created_at <= ?", dateFrom, dateTo) + + if outletID != nil { + query = query.Where("o.outlet_id = ?", *outletID) + } + + err := query. + Group("date"). + Order("date ASC"). + Scan(&results).Error + + return results, err +} + +func (r *AnalyticsRepositoryImpl) GetProductAnalytics(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time, limit int) ([]*entities.ProductAnalytics, error) { + var results []*entities.ProductAnalytics + + query := r.db.WithContext(ctx). + Table("order_items oi"). + Select(` + p.id as product_id, + p.name as product_name, + c.id as category_id, + c.name as category_name, + COALESCE(SUM(oi.quantity), 0) as quantity_sold, + COALESCE(SUM(oi.total_price), 0) as revenue, + CASE + WHEN SUM(oi.quantity) > 0 THEN COALESCE(SUM(oi.total_price), 0) / SUM(oi.quantity) + ELSE 0 + END as average_price, + COUNT(DISTINCT oi.order_id) as order_count + `). + Joins("JOIN products p ON oi.product_id = p.id"). + Joins("JOIN categories c ON p.category_id = c.id"). + Joins("JOIN orders o ON oi.order_id = o.id"). + Where("o.organization_id = ?", organizationID). + Where("o.is_void = ?", false). + Where("o.created_at >= ? AND o.created_at <= ?", dateFrom, dateTo) + + if outletID != nil { + query = query.Where("o.outlet_id = ?", *outletID) + } + + err := query. + Group("p.id, p.name, c.id, c.name"). + Order("revenue DESC"). + Limit(limit). + Scan(&results).Error + + return results, err +} + +func (r *AnalyticsRepositoryImpl) GetDashboardOverview(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time) (*entities.DashboardOverview, error) { + var result entities.DashboardOverview + + query := r.db.WithContext(ctx). + Table("orders o"). + Select(` + COALESCE(SUM(CASE WHEN o.is_void = false THEN o.total_amount ELSE 0 END), 0) as total_sales, + COUNT(CASE WHEN o.is_void = false THEN o.id END) as total_orders, + CASE + WHEN COUNT(CASE WHEN o.is_void = false THEN o.id END) > 0 + THEN COALESCE(SUM(CASE WHEN o.is_void = false THEN o.total_amount ELSE 0 END), 0) / COUNT(CASE WHEN o.is_void = false THEN o.id END) + ELSE 0 + END as average_order_value, + COUNT(DISTINCT o.customer_id) as total_customers, + COUNT(CASE WHEN o.is_void = true THEN o.id END) as voided_orders, + COUNT(CASE WHEN o.is_refund = true THEN o.id END) as refunded_orders + `). + Where("o.organization_id = ?", organizationID). + Where("o.created_at >= ? AND o.created_at <= ?", dateFrom, dateTo) + + if outletID != nil { + query = query.Where("o.outlet_id = ?", *outletID) + } + + err := query.Scan(&result).Error + if err != nil { + return nil, err + } + + return &result, nil +} + +func (r *AnalyticsRepositoryImpl) GetProfitLossAnalytics(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, dateFrom, dateTo time.Time, groupBy string) (*entities.ProfitLossAnalytics, error) { + // Summary query + var summary entities.ProfitLossSummary + + summaryQuery := r.db.WithContext(ctx). + Table("orders o"). + Select(` + COALESCE(SUM(o.total_amount), 0) as total_revenue, + COALESCE(SUM(o.total_cost), 0) as total_cost, + COALESCE(SUM(o.total_amount - o.total_cost), 0) as gross_profit, + CASE + WHEN SUM(o.total_amount) > 0 + THEN (SUM(o.total_amount - o.total_cost) / SUM(o.total_amount)) * 100 + ELSE 0 + END as gross_profit_margin, + COALESCE(SUM(o.tax_amount), 0) as total_tax, + COALESCE(SUM(o.discount_amount), 0) as total_discount, + COALESCE(SUM(o.total_amount - o.total_cost - o.discount_amount), 0) as net_profit, + CASE + WHEN SUM(o.total_amount) > 0 + THEN (SUM(o.total_amount - o.total_cost - o.discount_amount) / SUM(o.total_amount)) * 100 + ELSE 0 + END as net_profit_margin, + COUNT(o.id) as total_orders, + CASE + WHEN COUNT(o.id) > 0 + THEN SUM(o.total_amount - o.total_cost - o.discount_amount) / COUNT(o.id) + ELSE 0 + END as average_profit, + CASE + WHEN SUM(o.total_cost) > 0 + THEN (SUM(o.total_amount - o.total_cost) / SUM(o.total_cost)) * 100 + ELSE 0 + END as profitability_ratio + `). + Where("o.organization_id = ?", organizationID). + Where("o.status = ?", entities.OrderStatusCompleted). + Where("o.payment_status = ?", entities.PaymentStatusCompleted). + Where("o.is_void = false AND o.is_refund = false"). + Where("o.created_at >= ? AND o.created_at <= ?", dateFrom, dateTo) + + if outletID != nil { + summaryQuery = summaryQuery.Where("o.outlet_id = ?", *outletID) + } + + err := summaryQuery.Scan(&summary).Error + if err != nil { + return nil, err + } + + // Time series data query + var timeFormat string + switch groupBy { + case "hour": + timeFormat = "DATE_TRUNC('hour', o.created_at)" + case "week": + timeFormat = "DATE_TRUNC('week', o.created_at)" + case "month": + timeFormat = "DATE_TRUNC('month', o.created_at)" + default: // day + timeFormat = "DATE_TRUNC('day', o.created_at)" + } + + var data []entities.ProfitLossData + + dataQuery := r.db.WithContext(ctx). + Table("orders o"). + Select(` + `+timeFormat+` as date, + COALESCE(SUM(o.total_amount), 0) as revenue, + COALESCE(SUM(o.total_cost), 0) as cost, + COALESCE(SUM(o.total_amount - o.total_cost), 0) as gross_profit, + CASE + WHEN SUM(o.total_amount) > 0 + THEN (SUM(o.total_amount - o.total_cost) / SUM(o.total_amount)) * 100 + ELSE 0 + END as gross_profit_margin, + COALESCE(SUM(o.tax_amount), 0) as tax, + COALESCE(SUM(o.discount_amount), 0) as discount, + COALESCE(SUM(o.total_amount - o.total_cost - o.discount_amount), 0) as net_profit, + CASE + WHEN SUM(o.total_amount) > 0 + THEN (SUM(o.total_amount - o.total_cost - o.discount_amount) / SUM(o.total_amount)) * 100 + ELSE 0 + END as net_profit_margin, + COUNT(o.id) as orders + `). + Where("o.organization_id = ?", organizationID). + Where("o.status = ?", entities.OrderStatusCompleted). + Where("o.payment_status = ?", entities.PaymentStatusCompleted). + Where("o.is_void = false AND o.is_refund = false"). + Where("o.created_at >= ? AND o.created_at <= ?", dateFrom, dateTo). + Group(timeFormat). + Order(timeFormat) + + if outletID != nil { + dataQuery = dataQuery.Where("o.outlet_id = ?", *outletID) + } + + err = dataQuery.Scan(&data).Error + if err != nil { + return nil, err + } + + // Product profit data query + var productData []entities.ProductProfitData + + productQuery := r.db.WithContext(ctx). + Table("order_items oi"). + Select(` + p.id as product_id, + p.name as product_name, + c.id as category_id, + c.name as category_name, + SUM(oi.quantity) as quantity_sold, + SUM(oi.total_price) as revenue, + SUM(oi.total_cost) as cost, + SUM(oi.total_price - oi.total_cost) as gross_profit, + CASE + WHEN SUM(oi.total_price) > 0 + THEN (SUM(oi.total_price - oi.total_cost) / SUM(oi.total_price)) * 100 + ELSE 0 + END as gross_profit_margin, + AVG(oi.unit_price) as average_price, + AVG(oi.unit_cost) as average_cost, + AVG(oi.unit_price - oi.unit_cost) as profit_per_unit + `). + Joins("JOIN orders o ON oi.order_id = o.id"). + Joins("JOIN products p ON oi.product_id = p.id"). + Joins("JOIN categories c ON p.category_id = c.id"). + Where("o.organization_id = ?", organizationID). + Where("o.status = ?", entities.OrderStatusCompleted). + Where("o.payment_status = ?", entities.PaymentStatusCompleted). + Where("o.is_void = false AND o.is_refund = false"). + Where("o.created_at >= ? AND o.created_at <= ?", dateFrom, dateTo). + Group("p.id, p.name, c.id, c.name"). + Order("gross_profit DESC"). + Limit(20) + + if outletID != nil { + productQuery = productQuery.Where("o.outlet_id = ?", *outletID) + } + + err = productQuery.Scan(&productData).Error + if err != nil { + return nil, err + } + + return &entities.ProfitLossAnalytics{ + Summary: summary, + Data: data, + ProductData: productData, + }, nil +} diff --git a/internal/repository/auth/exec.go b/internal/repository/auth/exec.go deleted file mode 100644 index 8832b06..0000000 --- a/internal/repository/auth/exec.go +++ /dev/null @@ -1 +0,0 @@ -package auth diff --git a/internal/repository/auth/init.go b/internal/repository/auth/init.go deleted file mode 100644 index 8edf7b3..0000000 --- a/internal/repository/auth/init.go +++ /dev/null @@ -1,98 +0,0 @@ -package auth - -import ( - "context" - "errors" - "fmt" - "go.uber.org/zap" - "gorm.io/gorm" - - "enaklo-pos-be/internal/common/logger" - "enaklo-pos-be/internal/entity" -) - -type AuthRepository struct { - db *gorm.DB -} - -func NewAuthRepository(db *gorm.DB) *AuthRepository { - return &AuthRepository{ - db: db, - } -} - -func (r *AuthRepository) CheckExistsUserAccount(ctx context.Context, email string) (*entity.UserDB, error) { - var user entity.UserDB - - err := r.db. - Table("users"). - Select("users.*, user_roles.role_id, user_roles.partner_id, user_roles.site_id,"+ - " sites.name, roles.role_name, partners.name as partner_name, partners.status as partner_status, users.reset_password"). - Where("users.email = ?", email). - Joins("left join user_roles on users.id = user_roles.user_id"). - Joins("left join roles on user_roles.role_id = roles.role_id"). - Joins("left join partners on user_roles.partner_id = partners.id"). - Joins("left join sites on user_roles.site_id = sites.id"). - First(&user).Error - - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, nil - } - - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fmt.Errorf("user with email %s does not exist", email) // or use a custom error type - } - - logger.ContextLogger(ctx).Error(fmt.Sprintf("Failed to get user with email: %s", email), zap.Error(err)) - return nil, err - } - - return &user, nil -} - -func (r *AuthRepository) CheckExistsUserAccountByID(ctx context.Context, userID int64) (*entity.UserDB, error) { - var user entity.UserDB - - err := r.db. - Table("users"). - Select("users.*, user_roles.role_id, user_roles.partner_id, user_roles.site_id, sites.name, roles.role_name, partners.name as partner_name, partners.status as partner_status"). - Where("users.id = ?", userID). - Joins("left join user_roles on users.id = user_roles.user_id"). - Joins("left join roles on user_roles.role_id = roles.role_id"). - Joins("left join partners on user_roles.partner_id = partners.id"). - Joins("left join sites on user_roles.site_id = sites.id"). - First(&user).Error - - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, nil - } - - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fmt.Errorf("user with not exist") // or use a custom error type - } - - logger.ContextLogger(ctx).Error(fmt.Sprintf("Failed to get user"), zap.Error(err)) - return nil, err - } - - return &user, nil -} - -func (r *AuthRepository) UpdatePassword(ctx context.Context, trx *gorm.DB, newHashedPassword string, userID int64, resetPassword bool) error { - // Perform the update using a single Updates call - err := trx.Model(&entity.UserDB{}). - Where("id = ?", userID). - Updates(map[string]interface{}{ - "password": newHashedPassword, - "reset_password": resetPassword, - }).Error - - if err != nil { - logger.ContextLogger(ctx).Error(fmt.Sprintf("Failed to update password for user with id: %d", userID), zap.Error(err)) - return err - } - - return nil -} diff --git a/internal/repository/brevo/init.go b/internal/repository/brevo/init.go deleted file mode 100644 index 2e7dae4..0000000 --- a/internal/repository/brevo/init.go +++ /dev/null @@ -1,109 +0,0 @@ -package brevo - -import ( - "bytes" - "context" - "enaklo-pos-be/internal/common/logger" - "enaklo-pos-be/internal/entity" - "go.uber.org/zap" - "html/template" - "io/ioutil" - "log" - - brevo "github.com/getbrevo/brevo-go/lib" -) - -type Config interface { - GetApiKey() string -} - -type ServiceImpl struct { - brevoConn *brevo.APIClient -} - -func (s ServiceImpl) SendEmailTransactional(ctx context.Context, param entity.SendEmailNotificationParam) error { - templateFile, err := ioutil.ReadFile(param.TemplatePath) - if err != nil { - logger.ContextLogger(ctx).Error("error when reading file template", zap.Error(err)) - return err - } - - tmpl := template.New(param.TemplateName).Funcs(template.FuncMap{ - "range": func(args ...interface{}) []interface{} { - if len(args) == 0 { - return nil - } - - switch items := args[0].(type) { - case []map[string]string: - result := make([]interface{}, len(items)) - for i, item := range items { - result[i] = item - } - return result - case []interface{}: - return items - default: - if slice, ok := args[0].([]interface{}); ok { - return slice - } - return nil - } - }, - }) - - renderedTemplate, err := tmpl.Parse(string(templateFile)) - if err != nil { - log.Println(err) - return err - } - - return s.sendEmail(ctx, renderedTemplate, param) -} - -func (s ServiceImpl) sendEmail(ctx context.Context, tmpl *template.Template, param entity.SendEmailNotificationParam) error { - var body bytes.Buffer - err := tmpl.Execute(&body, param.Data) - if err != nil { - log.Println(err) - return err - } - - payload := brevo.SendSmtpEmail{ - Sender: &brevo.SendSmtpEmailSender{ - Name: "Enaklo", - Email: param.Sender, - }, - To: []brevo.SendSmtpEmailTo{ - { - Email: param.Recipient, - }, - }, - Subject: param.Subject, - HtmlContent: body.String(), - } - - if len(param.CcEmails) != 0 { - for _, email := range param.CcEmails { - payload.Cc = append(payload.Cc, brevo.SendSmtpEmailCc{ - Email: email, - }) - } - } - - _, _, err = s.brevoConn.TransactionalEmailsApi.SendTransacEmail(ctx, payload) - if err != nil { - logger.ContextLogger(ctx).Error("error when sending email", zap.Error(err)) - return err - } - - logger.ContextLogger(ctx).Info("sending email success") - return nil -} - -func New(conf Config) *ServiceImpl { - cfg := brevo.NewConfiguration() - cfg.AddDefaultHeader("api-key", conf.GetApiKey()) - client := brevo.NewAPIClient(cfg) - return &ServiceImpl{brevoConn: client} -} diff --git a/internal/repository/casheer_seasion.go b/internal/repository/casheer_seasion.go deleted file mode 100644 index 838a6cf..0000000 --- a/internal/repository/casheer_seasion.go +++ /dev/null @@ -1,177 +0,0 @@ -package repository - -import ( - "enaklo-pos-be/internal/common/mycontext" - "enaklo-pos-be/internal/entity" - "enaklo-pos-be/internal/repository/models" - "time" - - "github.com/pkg/errors" - "gorm.io/gorm" -) - -type CashierSessionRepository interface { - CreateSession(ctx mycontext.Context, session *entity.CashierSession) (*entity.CashierSession, error) - CloseSession(ctx mycontext.Context, sessionID int64, closingAmount, expectedAmount float64) error - GetOpenSessionByCashierID(ctx mycontext.Context, cashierID int64) (*entity.CashierSession, error) - GetSessionByID(ctx mycontext.Context, sessionID int64) (*entity.CashierSession, error) - GetPaymentSummaryBySessionID(ctx mycontext.Context, sessionID int64) ([]entity.PaymentSummary, error) - GetSessionHistoryByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int) ([]*entity.CashierSession, int64, error) -} - -type cashierSessionRepository struct { - db *gorm.DB -} - -func NewCashierSessionRepository(db *gorm.DB) CashierSessionRepository { - return &cashierSessionRepository{db: db} -} - -func (r *cashierSessionRepository) CreateSession(ctx mycontext.Context, session *entity.CashierSession) (*entity.CashierSession, error) { - dbModel := models.CashierSessionDB{ - PartnerID: session.PartnerID, - CashierID: session.CashierID, - OpenedAt: time.Now(), - OpeningAmount: session.OpeningAmount, - Status: "open", - CreatedAt: time.Now(), - } - - if err := r.db.Create(&dbModel).Error; err != nil { - return nil, errors.Wrap(err, "failed to create cashier session") - } - - session.ID = dbModel.ID - session.Status = dbModel.Status - session.OpenedAt = dbModel.OpenedAt - - return session, nil -} - -func (r *cashierSessionRepository) CloseSession(ctx mycontext.Context, sessionID int64, closingAmount, expectedAmount float64) error { - result := r.db.Model(&models.CashierSessionDB{}). - Where("id = ?", sessionID). - Updates(map[string]interface{}{ - "closed_at": time.Now(), - "closing_amount": closingAmount, - "expected_amount": expectedAmount, - "status": "closed", - }) - - if result.Error != nil { - return errors.Wrap(result.Error, "failed to close session") - } - - if result.RowsAffected == 0 { - return errors.New("no session updated") - } - - return nil -} - -func (r *cashierSessionRepository) GetOpenSessionByCashierID(ctx mycontext.Context, cashierID int64) (*entity.CashierSession, error) { - var dbModel models.CashierSessionDB - if err := r.db.Where("cashier_id = ? AND status = 'open'", cashierID). - Order("opened_at DESC").First(&dbModel).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, nil - } - return nil, errors.Wrap(err, "failed to get open session") - } - - return r.toEntity(&dbModel), nil -} - -func (r *cashierSessionRepository) GetSessionByID(ctx mycontext.Context, sessionID int64) (*entity.CashierSession, error) { - var dbModel models.CashierSessionDB - if err := r.db.First(&dbModel, sessionID).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, nil - } - return nil, errors.Wrap(err, "failed to get session by ID") - } - - return r.toEntity(&dbModel), nil -} - -func (r *cashierSessionRepository) toEntity(db *models.CashierSessionDB) *entity.CashierSession { - return &entity.CashierSession{ - ID: db.ID, - PartnerID: db.PartnerID, - CashierID: db.CashierID, - OpenedAt: db.OpenedAt, - ClosedAt: db.ClosedAt, - OpeningAmount: db.OpeningAmount, - ClosingAmount: db.ClosingAmount, - ExpectedAmount: db.ExpectedAmount, - Status: db.Status, - } -} - -func (r *cashierSessionRepository) GetPaymentSummaryBySessionID(ctx mycontext.Context, sessionID int64) ([]entity.PaymentSummary, error) { - type result struct { - PaymentType string - PaymentProvider string - TotalAmount float64 - } - - var rows []result - - err := r.db.WithContext(ctx). - Table("orders"). - Select("payment_type, payment_provider, SUM(total) AS total_amount"). - Where("cashier_session_id = ?", sessionID). - Group("payment_type, payment_provider"). - Scan(&rows).Error - - if err != nil { - return nil, errors.Wrap(err, "failed to summarize payments from orders") - } - - summary := make([]entity.PaymentSummary, len(rows)) - for i, row := range rows { - summary[i] = entity.PaymentSummary{ - PaymentType: row.PaymentType, - PaymentProvider: row.PaymentProvider, - TotalAmount: row.TotalAmount, - } - } - - return summary, nil -} - -func (r *cashierSessionRepository) GetSessionHistoryByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int) ([]*entity.CashierSession, int64, error) { - var sessionsDB []models.CashierSessionDB - var totalCount int64 - - // Count total records - if err := r.db.Model(&models.CashierSessionDB{}). - Where("partner_id = ?", partnerID). - Count(&totalCount).Error; err != nil { - return nil, 0, errors.Wrap(err, "failed to count cashier sessions") - } - - // Get sessions with pagination - query := r.db.Where("partner_id = ?", partnerID). - Order("opened_at DESC") - - if limit > 0 { - query = query.Limit(limit) - } - - if offset > 0 { - query = query.Offset(offset) - } - - if err := query.Find(&sessionsDB).Error; err != nil { - return nil, 0, errors.Wrap(err, "failed to get cashier session history") - } - - // Convert to entity - sessions := make([]*entity.CashierSession, len(sessionsDB)) - for i, sessionDB := range sessionsDB { - sessions[i] = r.toEntity(&sessionDB) - } - - return sessions, totalCount, nil -} diff --git a/internal/repository/categories_repo.go b/internal/repository/categories_repo.go deleted file mode 100644 index b02af20..0000000 --- a/internal/repository/categories_repo.go +++ /dev/null @@ -1,94 +0,0 @@ -package repository - -import ( - "enaklo-pos-be/internal/common/mycontext" - "enaklo-pos-be/internal/entity" - "enaklo-pos-be/internal/repository/models" - "github.com/pkg/errors" - "gorm.io/gorm" - "time" -) - -type CategoryRepository interface { - Create(ctx mycontext.Context, category *entity.Category) (*entity.Category, error) - GetByPartnerID(ctx mycontext.Context, partnerID int64) ([]*entity.Category, error) - GetByID(ctx mycontext.Context, id int64) (*entity.Category, error) - Update(ctx mycontext.Context, category *entity.Category) error - Delete(ctx mycontext.Context, id int64) error -} - -type categoryRepository struct { - db *gorm.DB -} - -func NewCategoryRepository(db *gorm.DB) CategoryRepository { - return &categoryRepository{db: db} -} - -func (r *categoryRepository) Create(ctx mycontext.Context, category *entity.Category) (*entity.Category, error) { - dbModel := &models.CategoryDB{ - PartnerID: category.PartnerID, - Name: category.Name, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - } - if err := r.db.WithContext(ctx).Create(dbModel).Error; err != nil { - return nil, errors.Wrap(err, "failed to create category") - } - category.ID = dbModel.ID - return category, nil -} - -func (r *categoryRepository) GetByPartnerID(ctx mycontext.Context, partnerID int64) ([]*entity.Category, error) { - var dbModels []models.CategoryDB - if err := r.db.WithContext(ctx). - Where("partner_id = ? AND deleted_at IS NULL", partnerID). - Find(&dbModels).Error; err != nil { - return nil, errors.Wrap(err, "failed to fetch categories by partner ID") - } - - var result []*entity.Category - for _, db := range dbModels { - result = append(result, r.toEntity(&db)) - } - return result, nil -} - -func (r *categoryRepository) GetByID(ctx mycontext.Context, id int64) (*entity.Category, error) { - var db models.CategoryDB - if err := r.db.WithContext(ctx). - Where("id = ? AND deleted_at IS NULL", id). - First(&db).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, nil - } - return nil, errors.Wrap(err, "failed to get category by ID") - } - return r.toEntity(&db), nil -} - -func (r *categoryRepository) Update(ctx mycontext.Context, category *entity.Category) error { - return r.db.WithContext(ctx).Model(&models.CategoryDB{}). - Where("id = ?", category.ID). - Updates(map[string]interface{}{ - "name": category.Name, - "updated_at": time.Now(), - }).Error -} - -func (r *categoryRepository) Delete(ctx mycontext.Context, id int64) error { - return r.db.WithContext(ctx). - Model(&models.CategoryDB{}). - Where("id = ?", id). - Update("deleted_at", time.Now()).Error -} - -func (r *categoryRepository) toEntity(db *models.CategoryDB) *entity.Category { - return &entity.Category{ - ID: db.ID, - PartnerID: db.PartnerID, - Name: db.Name, - CreatedAt: db.CreatedAt.Unix(), - UpdatedAt: db.UpdatedAt.Unix(), - } -} diff --git a/internal/repository/category_repository.go b/internal/repository/category_repository.go new file mode 100644 index 0000000..2afac4b --- /dev/null +++ b/internal/repository/category_repository.go @@ -0,0 +1,125 @@ +package repository + +import ( + "context" + + "apskel-pos-be/internal/entities" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type CategoryRepositoryImpl struct { + db *gorm.DB +} + +func NewCategoryRepositoryImpl(db *gorm.DB) *CategoryRepositoryImpl { + return &CategoryRepositoryImpl{ + db: db, + } +} + +func (r *CategoryRepositoryImpl) Create(ctx context.Context, category *entities.Category) error { + return r.db.WithContext(ctx).Create(category).Error +} + +func (r *CategoryRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID) (*entities.Category, error) { + var category entities.Category + err := r.db.WithContext(ctx).First(&category, "id = ?", id).Error + if err != nil { + return nil, err + } + return &category, nil +} + +func (r *CategoryRepositoryImpl) GetWithProducts(ctx context.Context, id uuid.UUID) (*entities.Category, error) { + var category entities.Category + err := r.db.WithContext(ctx).Preload("Products").First(&category, "id = ?", id).Error + if err != nil { + return nil, err + } + return &category, nil +} + +func (r *CategoryRepositoryImpl) GetByOrganization(ctx context.Context, organizationID uuid.UUID) ([]*entities.Category, error) { + var categories []*entities.Category + err := r.db.WithContext(ctx).Where("organization_id = ?", organizationID).Find(&categories).Error + return categories, err +} + +func (r *CategoryRepositoryImpl) GetByBusinessType(ctx context.Context, businessType string) ([]*entities.Category, error) { + var categories []*entities.Category + err := r.db.WithContext(ctx).Where("business_type = ?", businessType).Find(&categories).Error + return categories, err +} + +func (r *CategoryRepositoryImpl) Update(ctx context.Context, category *entities.Category) error { + return r.db.WithContext(ctx).Save(category).Error +} + +func (r *CategoryRepositoryImpl) Delete(ctx context.Context, id uuid.UUID) error { + return r.db.WithContext(ctx).Delete(&entities.Category{}, "id = ?", id).Error +} + +func (r *CategoryRepositoryImpl) List(ctx context.Context, filters map[string]interface{}, limit, offset int) ([]*entities.Category, int64, error) { + var categories []*entities.Category + var total int64 + + query := r.db.WithContext(ctx).Model(&entities.Category{}) + + for key, value := range filters { + switch key { + case "search": + searchValue := "%" + value.(string) + "%" + query = query.Where("name ILIKE ? OR description ILIKE ?", searchValue, searchValue) + default: + query = query.Where(key+" = ?", value) + } + } + + if err := query.Count(&total).Error; err != nil { + return nil, 0, err + } + + err := query.Limit(limit).Offset(offset).Find(&categories).Error + return categories, total, err +} + +func (r *CategoryRepositoryImpl) Count(ctx context.Context, filters map[string]interface{}) (int64, error) { + var count int64 + query := r.db.WithContext(ctx).Model(&entities.Category{}) + + for key, value := range filters { + switch key { + case "search": + searchValue := "%" + value.(string) + "%" + query = query.Where("name ILIKE ? OR description ILIKE ?", searchValue, searchValue) + default: + query = query.Where(key+" = ?", value) + } + } + + err := query.Count(&count).Error + return count, err +} + +func (r *CategoryRepositoryImpl) GetByName(ctx context.Context, organizationID uuid.UUID, name string) (*entities.Category, error) { + var category entities.Category + err := r.db.WithContext(ctx).Where("organization_id = ? AND name = ?", organizationID, name).First(&category).Error + if err != nil { + return nil, err + } + return &category, nil +} + +func (r *CategoryRepositoryImpl) ExistsByName(ctx context.Context, organizationID uuid.UUID, name string, excludeID *uuid.UUID) (bool, error) { + query := r.db.WithContext(ctx).Model(&entities.Category{}).Where("organization_id = ? AND name = ?", organizationID, name) + + if excludeID != nil { + query = query.Where("id != ?", *excludeID) + } + + var count int64 + err := query.Count(&count).Error + return count > 0, err +} diff --git a/internal/repository/crypto/crypto.go b/internal/repository/crypto/crypto.go deleted file mode 100644 index 4853e7a..0000000 --- a/internal/repository/crypto/crypto.go +++ /dev/null @@ -1,4 +0,0 @@ -//go:generate mockery --name Crypto --filename crypto.go --output ./mock --with-expecter - -package crypto - diff --git a/internal/repository/crypto/init.go b/internal/repository/crypto/init.go deleted file mode 100644 index ecdb4c7..0000000 --- a/internal/repository/crypto/init.go +++ /dev/null @@ -1,324 +0,0 @@ -package crypto - -import ( - "fmt" - "strconv" - "time" - - "github.com/golang-jwt/jwt" - "golang.org/x/crypto/bcrypt" - - "enaklo-pos-be/internal/common/errors" - "enaklo-pos-be/internal/entity" -) - -func NewCrypto(config CryptoConfig) *CryptoImpl { - return &CryptoImpl{ - Config: config, - } -} - -type CryptoConfig interface { - AccessTokenSecret() string - AccessTokenOrderSecret() string - AccessTokenOrderExpiresDate() time.Time - AccessTokenExpiresDate() time.Time - AccessTokenResetPasswordSecret() string - AccessTokenResetPasswordExpire() time.Time - AccessTokenWithdrawSecret() string - AccessTokenWithdrawExpire() time.Time - AccessTokenCustomerSecret() string -} - -type CryptoImpl struct { - Config CryptoConfig -} - -func (c *CryptoImpl) CompareHashAndPassword(hash string, password string) bool { - err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) - return err == nil -} - -func (c *CryptoImpl) ValidateWT(tokenString string) (*jwt.Token, error) { - token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { - if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { - return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) - } - return []byte(c.Config.AccessTokenSecret()), nil - }) - - return token, err -} - -func (c *CryptoImpl) GenerateJWT(user *entity.User) (string, error) { - partnerID := int64(0) - if user.PartnerID != nil { - partnerID = *user.PartnerID - } - - siteID := int64(0) - if user.SiteID != nil { - siteID = *user.SiteID - } - - claims := &entity.JWTAuthClaims{ - StandardClaims: jwt.StandardClaims{ - Subject: strconv.FormatInt(user.ID, 10), - ExpiresAt: c.Config.AccessTokenExpiresDate().Unix(), - IssuedAt: time.Now().Unix(), - NotBefore: time.Now().Unix(), - }, - UserID: user.ID, - Name: user.Name, - Email: user.Email, - Role: int(user.RoleID), - PartnerID: partnerID, - SiteID: siteID, - SiteName: user.SiteName, - } - - token, err := jwt. - NewWithClaims(jwt.SigningMethodHS256, claims). - SignedString([]byte(c.Config.AccessTokenSecret())) - - if err != nil { - return "", err - } - - return token, nil -} - -func (c *CryptoImpl) GenerateJWTReseetPassword(user *entity.User) (string, error) { - claims := &entity.JWTAuthClaims{ - StandardClaims: jwt.StandardClaims{ - Subject: strconv.FormatInt(user.ID, 10), - ExpiresAt: c.Config.AccessTokenResetPasswordExpire().Unix(), - IssuedAt: time.Now().Unix(), - NotBefore: time.Now().Unix(), - }, - UserID: user.ID, - Name: user.Name, - Email: user.Email, - } - - token, err := jwt. - NewWithClaims(jwt.SigningMethodHS256, claims). - SignedString([]byte(c.Config.AccessTokenResetPasswordSecret())) - - if err != nil { - return "", err - } - - return token, nil -} - -func (c *CryptoImpl) ParseAndValidateJWT(tokenString string) (*entity.JWTAuthClaims, error) { - token, err := jwt.ParseWithClaims(tokenString, &entity.JWTAuthClaims{}, func(token *jwt.Token) (interface{}, error) { - return []byte(c.Config.AccessTokenSecret()), nil - }) - if err != nil { - return nil, err - } - - if claims, ok := token.Claims.(*entity.JWTAuthClaims); ok && token.Valid { - return claims, nil - } else { - return nil, errors.ErrorUnauthorized - } -} - -func (c *CryptoImpl) ParseAndValidateJWTCustomer(tokenString string) (*entity.JWTAuthClaimsCustomer, error) { - token, err := jwt.ParseWithClaims(tokenString, &entity.JWTAuthClaimsCustomer{}, func(token *jwt.Token) (interface{}, error) { - return []byte(c.Config.AccessTokenCustomerSecret()), nil - }) - if err != nil { - return nil, err - } - - if claims, ok := token.Claims.(*entity.JWTAuthClaimsCustomer); ok && token.Valid { - return claims, nil - } else { - return nil, errors.ErrorUnauthorized - } -} - -func (c *CryptoImpl) GenerateJWTOrder(order *entity.Order) (string, error) { - claims := &entity.JWTOrderClaims{ - StandardClaims: jwt.StandardClaims{ - Subject: strconv.FormatInt(order.ID, 10), - ExpiresAt: c.Config.AccessTokenOrderExpiresDate().Unix(), - IssuedAt: time.Now().Unix(), - NotBefore: time.Now().Unix(), - }, - PartnerID: order.PartnerID, - OrderID: order.ID, - } - - token, err := jwt. - NewWithClaims(jwt.SigningMethodHS256, claims). - SignedString([]byte(c.Config.AccessTokenOrderSecret())) - - if err != nil { - return "", err - } - - return token, nil -} - -func (c *CryptoImpl) GenerateJWTOrderInquiry(inquiry *entity.OrderInquiry) (string, error) { - claims := &entity.JWTOrderClaims{ - StandardClaims: jwt.StandardClaims{ - Subject: inquiry.ID, - ExpiresAt: c.Config.AccessTokenOrderExpiresDate().Unix(), - IssuedAt: time.Now().Unix(), - NotBefore: time.Now().Unix(), - }, - PartnerID: inquiry.PartnerID, - InquiryID: inquiry.ID, - } - - token, err := jwt. - NewWithClaims(jwt.SigningMethodHS256, claims). - SignedString([]byte(c.Config.AccessTokenOrderSecret())) - - if err != nil { - return "", err - } - - return token, nil -} - -func (c *CryptoImpl) ValidateJWTOrderInquiry(tokenString string) (int64, string, error) { - token, err := jwt.ParseWithClaims(tokenString, &entity.JWTOrderClaims{}, func(token *jwt.Token) (interface{}, error) { - if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { - return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) - } - return []byte(c.Config.AccessTokenOrderSecret()), nil - }) - - if err != nil { - return 0, "", err - } - - claims, ok := token.Claims.(*entity.JWTOrderClaims) - if !ok || !token.Valid { - return 0, "", fmt.Errorf("invalid token %v", token.Header["alg"]) - } - - return claims.PartnerID, claims.InquiryID, nil -} - -func (c *CryptoImpl) ValidateJWTOrder(tokenString string) (int64, int64, error) { - token, err := jwt.ParseWithClaims(tokenString, &entity.JWTOrderClaims{}, func(token *jwt.Token) (interface{}, error) { - if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { - return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) - } - return []byte(c.Config.AccessTokenOrderSecret()), nil - }) - - if err != nil { - return 0, 0, err - } - - claims, ok := token.Claims.(*entity.JWTOrderClaims) - if !ok || !token.Valid { - return 0, 0, fmt.Errorf("invalid token %v", token.Header["alg"]) - } - - return claims.PartnerID, claims.OrderID, nil -} - -func (c *CryptoImpl) ValidateResetPassword(tokenString string) (int64, error) { - token, err := jwt.ParseWithClaims(tokenString, &entity.JWTOrderClaims{}, func(token *jwt.Token) (interface{}, error) { - if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { - return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) - } - return []byte(c.Config.AccessTokenResetPasswordSecret()), nil - }) - - if err != nil { - return 0, err - } - - claims, ok := token.Claims.(*entity.JWTAuthClaims) - if !ok || !token.Valid { - return 0, fmt.Errorf("invalid token %v", token.Header["alg"]) - } - - return claims.UserID, nil -} - -func (c *CryptoImpl) GenerateJWTWithdraw(req *entity.WalletWithdrawRequest) (string, error) { - claims := &entity.JWTWithdrawClaims{ - StandardClaims: jwt.StandardClaims{ - Subject: strconv.FormatInt(req.ID, 10), - ExpiresAt: c.Config.AccessTokenWithdrawExpire().Unix(), - IssuedAt: time.Now().Unix(), - NotBefore: time.Now().Unix(), - }, - PartnerID: req.PartnerID, - Amount: req.Amount, - Fee: req.Fee, - Total: req.Total, - } - - token, err := jwt. - NewWithClaims(jwt.SigningMethodHS256, claims). - SignedString([]byte(c.Config.AccessTokenWithdrawSecret())) - - if err != nil { - return "", err - } - - return token, nil -} - -func (c *CryptoImpl) ValidateJWTWithdraw(tokenString string) (*entity.WalletWithdrawRequest, error) { - token, err := jwt.ParseWithClaims(tokenString, &entity.JWTWithdrawClaims{}, func(token *jwt.Token) (interface{}, error) { - if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { - return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) - } - return []byte(c.Config.AccessTokenWithdrawSecret()), nil - }) - - if err != nil { - return nil, err - } - - claims, ok := token.Claims.(*entity.JWTWithdrawClaims) - if !ok || !token.Valid { - return nil, fmt.Errorf("invalid token %v", token.Header["alg"]) - } - - return &entity.WalletWithdrawRequest{ - ID: claims.ID, - PartnerID: claims.PartnerID, - Total: claims.Total, - Amount: claims.Amount, - Fee: claims.Fee, - }, nil -} - -func (c *CryptoImpl) GenerateJWTCustomer(user *entity.Customer) (string, error) { - claims := &entity.JWTAuthClaimsCustomer{ - StandardClaims: jwt.StandardClaims{ - Subject: strconv.FormatInt(user.ID, 10), - ExpiresAt: c.Config.AccessTokenExpiresDate().Unix(), - IssuedAt: time.Now().Unix(), - NotBefore: time.Now().Unix(), - }, - UserID: user.ID, - Name: user.Name, - Email: user.Email, - } - - token, err := jwt. - NewWithClaims(jwt.SigningMethodHS256, claims). - SignedString([]byte(c.Config.AccessTokenCustomerSecret())) - - if err != nil { - return "", err - } - - return token, nil -} diff --git a/internal/repository/customer_repo.go b/internal/repository/customer_repo.go deleted file mode 100644 index bc0937b..0000000 --- a/internal/repository/customer_repo.go +++ /dev/null @@ -1,387 +0,0 @@ -package repository - -import ( - "enaklo-pos-be/internal/common/mycontext" - "enaklo-pos-be/internal/entity" - "enaklo-pos-be/internal/repository/models" - "fmt" - "github.com/google/uuid" - "github.com/pkg/errors" - "gorm.io/gorm" - "math/rand" - "time" -) - -type CustomerRepo interface { - Create(ctx mycontext.Context, customer *entity.Customer) (*entity.Customer, error) - FindByID(ctx mycontext.Context, id int64) (*entity.Customer, error) - FindByPhone(ctx mycontext.Context, phone string) (*entity.Customer, error) - FindByEmail(ctx mycontext.Context, email string) (*entity.Customer, error) - AddPoints(ctx mycontext.Context, id int64, points int, reference string) error - GetPointsByCustomerID( - ctx mycontext.Context, - customerID int64, - ) (*entity.CustomerPoints, error) - FindSequence(ctx mycontext.Context, partnerID int64) (int64, error) - GetAllCustomers(ctx mycontext.Context, req entity.MemberSearch) (entity.MemberList, int, error) - VerifyOTP(ctx mycontext.Context, verificationHash string, otpCode string) (int64, error) -} - -type customerRepository struct { - db *gorm.DB -} - -func NewCustomerRepository(db *gorm.DB) *customerRepository { - return &customerRepository{db: db} -} - -func (r *customerRepository) Create(ctx mycontext.Context, customer *entity.Customer) (*entity.Customer, error) { - tx := r.db.Begin() - if tx.Error != nil { - return nil, errors.Wrap(tx.Error, "failed to begin transaction") - } - - customerDB := r.toCustomerDBModel(customer) - if err := tx.Omit("CustomerID").Create(&customerDB).Error; err != nil { - tx.Rollback() - return nil, errors.Wrap(err, "failed to insert customer") - } - - customerPoints := models.CustomerPointsDB{ - CustomerID: uint64(customerDB.ID), - TotalPoints: 0, - AvailablePoints: 0, - LastUpdated: time.Now(), - } - - if err := tx.Create(&customerPoints).Error; err != nil { - tx.Rollback() - return nil, errors.Wrap(err, "failed to create initial customer points") - } - - otpCode := r.generateOTPCode() - expiresAt := time.Now().Add(15 * time.Minute) - - verificationCode := models.CustomerVerificationCodeDB{ - CustomerID: uint64(customerDB.ID), - Code: otpCode, - Type: "EMAIL", - ExpiresAt: expiresAt, - IsUsed: false, - VerificationID: uuid.New(), - } - - if err := tx.Create(&verificationCode).Error; err != nil { - tx.Rollback() - return nil, errors.Wrap(err, "failed to create verification code") - } - - if err := tx.Commit().Error; err != nil { - return nil, errors.Wrap(err, "failed to commit transaction") - } - - customer.ID = customerDB.ID - customer.VerificationID = verificationCode.VerificationID.String() - customer.OTP = otpCode - - return customer, nil -} - -func (r *customerRepository) FindByID(ctx mycontext.Context, id int64) (*entity.Customer, error) { - var customerDB models.CustomerDB - - if err := r.db.First(&customerDB, id).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, errors.New("customer not found") - } - return nil, errors.Wrap(err, "failed to find customer") - } - - customer := r.toDomainCustomerModel(&customerDB) - - return customer, nil -} - -func (r *customerRepository) FindByPhone(ctx mycontext.Context, phone string) (*entity.Customer, error) { - var customerDB models.CustomerDB - - if err := r.db.Where("phone = ?", phone).First(&customerDB).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, errors.New("customer not found") - } - return nil, errors.Wrap(err, "failed to find customer by phone") - } - - customer := r.toDomainCustomerModel(&customerDB) - - return customer, nil -} - -func (r *customerRepository) FindByEmail(ctx mycontext.Context, email string) (*entity.Customer, error) { - var customerDB models.CustomerDB - - if err := r.db.Where("email = ?", email).First(&customerDB).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, errors.New("customer not found") - } - return nil, errors.Wrap(err, "failed to find customer by email") - } - - customer := r.toDomainCustomerModel(&customerDB) - - return customer, nil -} - -func (r *customerRepository) AddPoints(ctx mycontext.Context, customerID int64, points int, reference string) error { - tx := r.db.Begin() - if tx.Error != nil { - return errors.Wrap(tx.Error, "failed to begin transaction") - } - - result := tx.Model(&models.CustomerPointsDB{}). - Where("customer_id = ?", customerID). - Updates(map[string]interface{}{ - "total_points": gorm.Expr("total_points + ?", points), - "available_points": gorm.Expr("available_points + ?", points), - "last_updated": time.Now(), - }) - - if result.Error != nil { - tx.Rollback() - return errors.Wrap(result.Error, "failed to update customer points") - } - - if result.RowsAffected == 0 { - tx.Rollback() - return errors.New("customer points record not found") - } - - pointTransaction := models.CustomerPointTransactionDB{ - CustomerID: customerID, - Reference: reference, - PointsEarned: points, - TransactionDate: time.Now(), - Status: "active", - } - - if err := tx.Create(&pointTransaction).Error; err != nil { - tx.Rollback() - return errors.Wrap(err, "failed to create point transaction record") - } - - if err := tx.Commit().Error; err != nil { - return errors.Wrap(err, "failed to commit transaction") - } - - return nil -} - -func (r *customerRepository) GetPointsByCustomerID( - ctx mycontext.Context, - customerID int64, -) (*entity.CustomerPoints, error) { - var cp models.CustomerPointsDB - - if err := r.db.Where("customer_id = ?", customerID).First(&cp).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, errors.New("customer not found") - } - return nil, errors.Wrap(err, "failed to find customer point by customer_id") - } - - return r.toDomainCustomerPoint(cp), nil -} - -func (r *customerRepository) toDomainCustomerPoint(dbModel models.CustomerPointsDB) *entity.CustomerPoints { - return &entity.CustomerPoints{ - ID: dbModel.ID, - CustomerID: dbModel.CustomerID, - TotalPoints: dbModel.TotalPoints, - AvailablePoints: dbModel.AvailablePoints, - } -} - -func (r *customerRepository) toCustomerDBModel(customer *entity.Customer) models.CustomerDB { - return models.CustomerDB{ - ID: customer.ID, - Name: customer.Name, - Email: customer.Email, - Phone: customer.Phone, - Points: customer.Points, - CreatedAt: customer.CreatedAt, - UpdatedAt: customer.UpdatedAt, - BirthDate: customer.BirthDate, - Password: customer.Password, - } -} - -func (r *customerRepository) FindSequence(ctx mycontext.Context, partnerID int64) (int64, error) { - tx := r.db.Begin() - if tx.Error != nil { - return 0, errors.Wrap(tx.Error, "failed to begin transaction") - } - - defer func() { - if r := recover(); r != nil { - tx.Rollback() - } - }() - - var sequence models.PartnerMemberSequence - - result := tx.Where("partner_id = ?", partnerID).First(&sequence) - if result.Error != nil { - if errors.Is(result.Error, gorm.ErrRecordNotFound) { - now := time.Now() - newSequence := models.PartnerMemberSequence{ - PartnerID: partnerID, - LastSequence: 1, - UpdatedAt: now, - } - - if err := tx.Create(&newSequence).Error; err != nil { - tx.Rollback() - return 0, errors.Wrap(err, "failed to create new sequence") - } - - if err := tx.Commit().Error; err != nil { - return 0, errors.Wrap(err, "failed to commit transaction") - } - - return 1, nil - } - - tx.Rollback() - return 0, errors.Wrap(result.Error, "failed to query sequence") - } - - newSequenceValue := sequence.LastSequence + 1 - updates := map[string]interface{}{ - "last_sequence": newSequenceValue, - "updated_at": time.Now(), - } - - if err := tx.Model(&sequence).Updates(updates).Error; err != nil { - tx.Rollback() - return 0, errors.Wrap(err, "failed to update sequence") - } - - if err := tx.Commit().Error; err != nil { - return 0, errors.Wrap(err, "failed to commit transaction") - } - - return newSequenceValue, nil -} - -func (r *customerRepository) toDomainCustomerModel(dbModel *models.CustomerDB) *entity.Customer { - return &entity.Customer{ - ID: dbModel.ID, - Name: dbModel.Name, - Email: dbModel.Email, - Phone: dbModel.Phone, - Points: dbModel.Points, - CreatedAt: dbModel.CreatedAt, - UpdatedAt: dbModel.UpdatedAt, - CustomerID: dbModel.CustomerID, - BirthDate: dbModel.BirthDate, - Password: dbModel.Password, - } -} - -func (r *customerRepository) GetAllCustomers(ctx mycontext.Context, req entity.MemberSearch) (entity.MemberList, int, error) { - if req.Limit <= 0 { - req.Limit = 10 - } - if req.Offset < 0 { - req.Offset = 0 - } - - query := r.db.Model(&models.CustomerDB{}) - - if req.Search != "" { - searchTerm := "%" + req.Search + "%" - query = query.Where( - "name ILIKE ? OR email ILIKE ? OR phone ILIKE ?", - searchTerm, searchTerm, searchTerm, - ) - } - - var totalCount int64 - if err := query.Count(&totalCount).Error; err != nil { - return nil, 0, errors.Wrap(err, "failed to count customers") - } - - var customersDB []models.CustomerDB - result := query. - Order("created_at DESC"). - Limit(req.Limit). - Offset(req.Offset). - Find(&customersDB) - - if result.Error != nil { - return nil, 0, errors.Wrap(result.Error, "failed to retrieve customers") - } - - customers := make(entity.MemberList, len(customersDB)) - for i, customerDB := range customersDB { - customers[i] = r.toDomainCustomerModel(&customerDB) - } - - return customers, int(totalCount), nil -} - -func (r *customerRepository) generateOTPCode() string { - rand.Seed(time.Now().UnixNano()) - otpCode := fmt.Sprintf("%06d", rand.Intn(1000000)) - return otpCode -} - -func (r *customerRepository) VerifyOTP(ctx mycontext.Context, verificationHash string, otpCode string) (int64, error) { - var verificationCode models.CustomerVerificationCodeDB - if err := r.db.Where("verification_id = ? AND is_used = false", verificationHash).First(&verificationCode).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return 0, errors.New("invalid or expired verification code") - } - return 0, errors.Wrap(err, "failed to find verification code") - } - - if time.Now().After(verificationCode.ExpiresAt) { - return 0, errors.New("verification code has expired") - } - - if verificationCode.Code != otpCode { - return 0, errors.New("invalid verification code") - } - - tx := r.db.Begin() - if tx.Error != nil { - return 0, errors.Wrap(tx.Error, "failed to begin transaction") - } - - if err := tx.Model(&verificationCode).Updates(map[string]interface{}{ - "is_used": true, - }).Error; err != nil { - tx.Rollback() - return 0, errors.Wrap(err, "failed to mark verification code as used") - } - - if verificationCode.Type == "EMAIL" { - if err := tx.Model(&models.CustomerDB{}).Where("id = ?", verificationCode.CustomerID). - Update("is_email_verified", true).Error; err != nil { - tx.Rollback() - return 0, errors.Wrap(err, "failed to update customer verification status") - } - } else if verificationCode.Type == "PHONE" { - if err := tx.Model(&models.CustomerDB{}).Where("id = ?", verificationCode.CustomerID). - Update("is_phone_verified", true).Error; err != nil { - tx.Rollback() - return 0, errors.Wrap(err, "failed to update customer verification status") - } - } - - if err := tx.Commit().Error; err != nil { - return 0, errors.Wrap(err, "failed to commit transaction") - } - - return int64(verificationCode.CustomerID), nil -} diff --git a/internal/repository/customer_repository.go b/internal/repository/customer_repository.go new file mode 100644 index 0000000..a92f0f4 --- /dev/null +++ b/internal/repository/customer_repository.go @@ -0,0 +1,140 @@ +package repository + +import ( + "apskel-pos-be/internal/entities" + "context" + "fmt" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type CustomerRepository struct { + db *gorm.DB +} + +func NewCustomerRepository(db *gorm.DB) *CustomerRepository { + return &CustomerRepository{db: db} +} + +func (r *CustomerRepository) Create(ctx context.Context, customer *entities.Customer) error { + return r.db.WithContext(ctx).Create(customer).Error +} + +func (r *CustomerRepository) GetByID(ctx context.Context, id uuid.UUID) (*entities.Customer, error) { + var customer entities.Customer + err := r.db.WithContext(ctx).Where("id = ?", id).First(&customer).Error + if err != nil { + return nil, err + } + return &customer, nil +} + +func (r *CustomerRepository) GetByIDAndOrganization(ctx context.Context, id, organizationID uuid.UUID) (*entities.Customer, error) { + var customer entities.Customer + err := r.db.WithContext(ctx).Where("id = ? AND organization_id = ?", id, organizationID).First(&customer).Error + if err != nil { + return nil, err + } + return &customer, nil +} + +func (r *CustomerRepository) GetDefaultCustomer(ctx context.Context, organizationID uuid.UUID) (*entities.Customer, error) { + var customer entities.Customer + err := r.db.WithContext(ctx).Where("organization_id = ? AND is_default = ?", organizationID, true).First(&customer).Error + if err != nil { + return nil, err + } + return &customer, nil +} + +func (r *CustomerRepository) List(ctx context.Context, organizationID uuid.UUID, offset, limit int, search string, isActive, isDefault *bool, sortBy, sortOrder string) ([]entities.Customer, int64, error) { + var customers []entities.Customer + var total int64 + + query := r.db.WithContext(ctx).Where("organization_id = ?", organizationID) + + if search != "" { + searchTerm := "%" + search + "%" + query = query.Where("name ILIKE ? OR email ILIKE ? OR phone ILIKE ?", searchTerm, searchTerm, searchTerm) + } + + if isActive != nil { + query = query.Where("is_active = ?", *isActive) + } + + if isDefault != nil { + query = query.Where("is_default = ?", *isDefault) + } + + if err := query.Model(&entities.Customer{}).Count(&total).Error; err != nil { + return nil, 0, err + } + + if sortBy != "" { + if sortOrder == "" { + sortOrder = "asc" + } + query = query.Order(fmt.Sprintf("%s %s", sortBy, sortOrder)) + } else { + query = query.Order("created_at DESC") + } + + err := query.Offset(offset).Limit(limit).Find(&customers).Error + if err != nil { + return nil, 0, err + } + + return customers, total, nil +} + +func (r *CustomerRepository) Update(ctx context.Context, customer *entities.Customer) error { + return r.db.WithContext(ctx).Save(customer).Error +} + +func (r *CustomerRepository) Delete(ctx context.Context, id uuid.UUID) error { + return r.db.WithContext(ctx).Delete(&entities.Customer{}, id).Error +} + +func (r *CustomerRepository) SetAsDefault(ctx context.Context, customerID, organizationID uuid.UUID) error { + return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + if err := tx.Model(&entities.Customer{}). + Where("organization_id = ? AND is_default = ?", organizationID, true). + Update("is_default", false).Error; err != nil { + return err + } + + if err := tx.Model(&entities.Customer{}). + Where("id = ? AND organization_id = ?", customerID, organizationID). + Update("is_default", true).Error; err != nil { + return err + } + + return nil + }) +} + +func (r *CustomerRepository) CreateDefaultCustomer(ctx context.Context, organizationID uuid.UUID) (*entities.Customer, error) { + defaultCustomer := &entities.Customer{ + OrganizationID: organizationID, + Name: "Walk In Customer", + IsDefault: true, + IsActive: true, + } + + err := r.db.WithContext(ctx).Create(defaultCustomer).Error + if err != nil { + return nil, err + } + + return defaultCustomer, nil +} + +func (r *CustomerRepository) GetByEmail(ctx context.Context, email string, organizationID uuid.UUID) (*entities.Customer, error) { + var customer entities.Customer + err := r.db.WithContext(ctx).Where("email = ? AND organization_id = ?", email, organizationID).First(&customer).Error + if err != nil { + return nil, err + } + return &customer, nil +} diff --git a/internal/repository/file_repository.go b/internal/repository/file_repository.go new file mode 100644 index 0000000..eec0abc --- /dev/null +++ b/internal/repository/file_repository.go @@ -0,0 +1,109 @@ +package repository + +import ( + "context" + "fmt" + + "apskel-pos-be/internal/entities" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type FileRepository interface { + Create(ctx context.Context, file *entities.File) error + GetByID(ctx context.Context, id uuid.UUID) (*entities.File, error) + GetByOrganizationID(ctx context.Context, organizationID uuid.UUID) ([]*entities.File, error) + GetByUserID(ctx context.Context, userID uuid.UUID) ([]*entities.File, error) + Update(ctx context.Context, file *entities.File) error + Delete(ctx context.Context, id uuid.UUID) error + List(ctx context.Context, filters map[string]interface{}, limit, offset int) ([]*entities.File, int64, error) + GetByFileName(ctx context.Context, fileName string) (*entities.File, error) + ExistsByFileName(ctx context.Context, fileName string) (bool, error) +} + +type FileRepositoryImpl struct { + db *gorm.DB +} + +func NewFileRepositoryImpl(db *gorm.DB) *FileRepositoryImpl { + return &FileRepositoryImpl{ + db: db, + } +} + +func (r *FileRepositoryImpl) Create(ctx context.Context, file *entities.File) error { + return r.db.WithContext(ctx).Create(file).Error +} + +func (r *FileRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID) (*entities.File, error) { + var file entities.File + err := r.db.WithContext(ctx).Where("id = ?", id).First(&file).Error + if err != nil { + return nil, err + } + return &file, nil +} + +func (r *FileRepositoryImpl) GetByOrganizationID(ctx context.Context, organizationID uuid.UUID) ([]*entities.File, error) { + var files []*entities.File + err := r.db.WithContext(ctx).Where("organization_id = ?", organizationID).Find(&files).Error + return files, err +} + +func (r *FileRepositoryImpl) GetByUserID(ctx context.Context, userID uuid.UUID) ([]*entities.File, error) { + var files []*entities.File + err := r.db.WithContext(ctx).Where("user_id = ?", userID).Find(&files).Error + return files, err +} + +func (r *FileRepositoryImpl) Update(ctx context.Context, file *entities.File) error { + return r.db.WithContext(ctx).Save(file).Error +} + +func (r *FileRepositoryImpl) Delete(ctx context.Context, id uuid.UUID) error { + return r.db.WithContext(ctx).Where("id = ?", id).Delete(&entities.File{}).Error +} + +func (r *FileRepositoryImpl) List(ctx context.Context, filters map[string]interface{}, limit, offset int) ([]*entities.File, int64, error) { + var files []*entities.File + var total int64 + + query := r.db.WithContext(ctx).Model(&entities.File{}) + + // Apply filters + for key, value := range filters { + if value != nil && value != "" { + query = query.Where(fmt.Sprintf("%s = ?", key), value) + } + } + + // Get total count + err := query.Count(&total).Error + if err != nil { + return nil, 0, err + } + + // Get paginated results + err = query.Offset(offset).Limit(limit).Order("created_at DESC").Find(&files).Error + if err != nil { + return nil, 0, err + } + + return files, total, nil +} + +func (r *FileRepositoryImpl) GetByFileName(ctx context.Context, fileName string) (*entities.File, error) { + var file entities.File + err := r.db.WithContext(ctx).Where("file_name = ?", fileName).First(&file).Error + if err != nil { + return nil, err + } + return &file, nil +} + +func (r *FileRepositoryImpl) ExistsByFileName(ctx context.Context, fileName string) (bool, error) { + var count int64 + err := r.db.WithContext(ctx).Model(&entities.File{}).Where("file_name = ?", fileName).Count(&count).Error + return count > 0, err +} diff --git a/internal/repository/inventory_repository.go b/internal/repository/inventory_repository.go new file mode 100644 index 0000000..2a77884 --- /dev/null +++ b/internal/repository/inventory_repository.go @@ -0,0 +1,301 @@ +package repository + +import ( + "context" + "errors" + "fmt" + + "apskel-pos-be/internal/entities" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type InventoryRepositoryImpl struct { + db *gorm.DB +} + +func NewInventoryRepositoryImpl(db *gorm.DB) *InventoryRepositoryImpl { + return &InventoryRepositoryImpl{ + db: db, + } +} + +func (r *InventoryRepositoryImpl) Create(ctx context.Context, inventory *entities.Inventory) error { + return r.db.WithContext(ctx).Create(inventory).Error +} + +func (r *InventoryRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID) (*entities.Inventory, error) { + var inventory entities.Inventory + err := r.db.WithContext(ctx).First(&inventory, "id = ?", id).Error + if err != nil { + return nil, err + } + return &inventory, nil +} + +func (r *InventoryRepositoryImpl) GetWithRelations(ctx context.Context, id uuid.UUID) (*entities.Inventory, error) { + var inventory entities.Inventory + err := r.db.WithContext(ctx). + Preload("Product"). + Preload("Product.Category"). + Preload("Outlet"). + First(&inventory, "id = ?", id).Error + if err != nil { + return nil, err + } + return &inventory, nil +} + +func (r *InventoryRepositoryImpl) GetByProductAndOutlet(ctx context.Context, productID, outletID uuid.UUID) (*entities.Inventory, error) { + var inventory entities.Inventory + err := r.db.WithContext(ctx).Where("product_id = ? AND outlet_id = ?", productID, outletID).First(&inventory).Error + if err != nil { + return nil, err + } + return &inventory, nil +} + +func (r *InventoryRepositoryImpl) GetByOutlet(ctx context.Context, outletID uuid.UUID) ([]*entities.Inventory, error) { + var inventory []*entities.Inventory + err := r.db.WithContext(ctx). + Preload("Product"). + Preload("Product.Category"). + Where("outlet_id = ?", outletID). + Find(&inventory).Error + return inventory, err +} + +func (r *InventoryRepositoryImpl) GetByProduct(ctx context.Context, productID uuid.UUID) ([]*entities.Inventory, error) { + var inventory []*entities.Inventory + err := r.db.WithContext(ctx). + Preload("Outlet"). + Where("product_id = ?", productID). + Find(&inventory).Error + return inventory, err +} + +func (r *InventoryRepositoryImpl) GetLowStock(ctx context.Context, outletID uuid.UUID) ([]*entities.Inventory, error) { + var inventory []*entities.Inventory + err := r.db.WithContext(ctx). + Preload("Product"). + Preload("Product.Category"). + Where("outlet_id = ? AND quantity <= reorder_level", outletID). + Find(&inventory).Error + return inventory, err +} + +func (r *InventoryRepositoryImpl) GetZeroStock(ctx context.Context, outletID uuid.UUID) ([]*entities.Inventory, error) { + var inventory []*entities.Inventory + err := r.db.WithContext(ctx). + Preload("Product"). + Preload("Product.Category"). + Where("outlet_id = ? AND quantity = 0", outletID). + Find(&inventory).Error + return inventory, err +} + +func (r *InventoryRepositoryImpl) Update(ctx context.Context, inventory *entities.Inventory) error { + return r.db.WithContext(ctx).Save(inventory).Error +} + +func (r *InventoryRepositoryImpl) Delete(ctx context.Context, id uuid.UUID) error { + return r.db.WithContext(ctx).Delete(&entities.Inventory{}, "id = ?", id).Error +} + +func (r *InventoryRepositoryImpl) List(ctx context.Context, filters map[string]interface{}, limit, offset int) ([]*entities.Inventory, int64, error) { + var inventory []*entities.Inventory + var total int64 + + query := r.db.WithContext(ctx).Model(&entities.Inventory{}). + Preload("Product"). + Preload("Product.Category"). + Preload("Outlet") + + for key, value := range filters { + switch key { + case "search": + searchValue := "%" + value.(string) + "%" + query = query.Joins("JOIN products ON inventory.product_id = products.id"). + Where("products.name ILIKE ? OR products.sku ILIKE ?", searchValue, searchValue) + case "low_stock": + if value.(bool) { + query = query.Where("quantity <= reorder_level") + } + case "zero_stock": + if value.(bool) { + query = query.Where("quantity = 0") + } + case "category_id": + query = query.Joins("JOIN products ON inventory.product_id = products.id"). + Where("products.category_id = ?", value) + default: + query = query.Where(key+" = ?", value) + } + } + + if err := query.Count(&total).Error; err != nil { + return nil, 0, err + } + + err := query.Limit(limit).Offset(offset).Find(&inventory).Error + return inventory, total, err +} + +func (r *InventoryRepositoryImpl) Count(ctx context.Context, filters map[string]interface{}) (int64, error) { + var count int64 + query := r.db.WithContext(ctx).Model(&entities.Inventory{}) + + for key, value := range filters { + switch key { + case "search": + searchValue := "%" + value.(string) + "%" + query = query.Joins("JOIN products ON inventory.product_id = products.id"). + Where("products.name ILIKE ? OR products.sku ILIKE ?", searchValue, searchValue) + case "low_stock": + if value.(bool) { + query = query.Where("quantity <= reorder_level") + } + case "zero_stock": + if value.(bool) { + query = query.Where("quantity = 0") + } + case "category_id": + query = query.Joins("JOIN products ON inventory.product_id = products.id"). + Where("products.category_id = ?", value) + default: + query = query.Where(key+" = ?", value) + } + } + + err := query.Count(&count).Error + return count, err +} + +func (r *InventoryRepositoryImpl) AdjustQuantity(ctx context.Context, productID, outletID uuid.UUID, delta int) (*entities.Inventory, error) { + var inventory entities.Inventory + + err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + // Try to find existing inventory + if err := tx.Where("product_id = ? AND outlet_id = ?", productID, outletID).First(&inventory).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + // Inventory doesn't exist, create it with initial quantity + inventory = entities.Inventory{ + ProductID: productID, + OutletID: outletID, + Quantity: 0, + ReorderLevel: 0, + } + if err := tx.Create(&inventory).Error; err != nil { + return fmt.Errorf("failed to create inventory record: %w", err) + } + } else { + return err + } + } + + inventory.UpdateQuantity(delta) + return tx.Save(&inventory).Error + }) + + if err != nil { + return nil, err + } + + return &inventory, nil +} + +func (r *InventoryRepositoryImpl) SetQuantity(ctx context.Context, productID, outletID uuid.UUID, quantity int) (*entities.Inventory, error) { + var inventory entities.Inventory + + err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + // Get current inventory + if err := tx.Where("product_id = ? AND outlet_id = ?", productID, outletID).First(&inventory).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + // Inventory doesn't exist, create it with the specified quantity + inventory = entities.Inventory{ + ProductID: productID, + OutletID: outletID, + Quantity: quantity, + ReorderLevel: 0, + } + if inventory.Quantity < 0 { + inventory.Quantity = 0 + } + if err := tx.Create(&inventory).Error; err != nil { + return fmt.Errorf("failed to create inventory record: %w", err) + } + return nil + } + return err + } + + // Set new quantity + inventory.Quantity = quantity + if inventory.Quantity < 0 { + inventory.Quantity = 0 + } + + // Save updated inventory + return tx.Save(&inventory).Error + }) + + if err != nil { + return nil, err + } + + return &inventory, nil +} + +func (r *InventoryRepositoryImpl) UpdateReorderLevel(ctx context.Context, id uuid.UUID, reorderLevel int) error { + return r.db.WithContext(ctx).Model(&entities.Inventory{}). + Where("id = ?", id). + Update("reorder_level", reorderLevel).Error +} + +func (r *InventoryRepositoryImpl) BulkCreate(ctx context.Context, inventoryItems []*entities.Inventory) error { + return r.db.WithContext(ctx).CreateInBatches(inventoryItems, 100).Error +} + +func (r *InventoryRepositoryImpl) BulkAdjustQuantity(ctx context.Context, adjustments map[uuid.UUID]int, outletID uuid.UUID) error { + return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + for productID, delta := range adjustments { + var inventory entities.Inventory + if err := tx.Where("product_id = ? AND outlet_id = ?", productID, outletID).First(&inventory).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + // Inventory doesn't exist, create it with initial quantity + inventory = entities.Inventory{ + ProductID: productID, + OutletID: outletID, + Quantity: 0, + ReorderLevel: 0, + } + if err := tx.Create(&inventory).Error; err != nil { + return fmt.Errorf("failed to create inventory record for product %s: %w", productID, err) + } + } else { + return err + } + } + + inventory.UpdateQuantity(delta) + + if err := tx.Save(&inventory).Error; err != nil { + return err + } + } + return nil + }) +} + +func (r *InventoryRepositoryImpl) GetTotalValueByOutlet(ctx context.Context, outletID uuid.UUID) (float64, error) { + var totalValue float64 + err := r.db.WithContext(ctx). + Table("inventory"). + Select("SUM(inventory.quantity * products.cost)"). + Joins("JOIN products ON inventory.product_id = products.id"). + Where("inventory.outlet_id = ?", outletID). + Scan(&totalValue).Error + + return totalValue, err +} diff --git a/internal/repository/license/license.go b/internal/repository/license/license.go deleted file mode 100644 index ace6467..0000000 --- a/internal/repository/license/license.go +++ /dev/null @@ -1,122 +0,0 @@ -package license - -import ( - "context" - "enaklo-pos-be/internal/common/logger" - "enaklo-pos-be/internal/entity" - "errors" - "github.com/google/uuid" - "go.uber.org/zap" - "gorm.io/gorm" - "time" -) - -type LicenseRepository struct { - db *gorm.DB -} - -func NewLicenseRepository(db *gorm.DB) *LicenseRepository { - return &LicenseRepository{ - db: db, - } -} - -func (r *LicenseRepository) Create(ctx context.Context, license *entity.LicenseDB) (*entity.LicenseDB, error) { - if err := r.db.WithContext(ctx).Create(license).Error; err != nil { - logger.ContextLogger(ctx).Error("error when creating license", zap.Error(err)) - return nil, err - } - return license, nil -} - -func (r *LicenseRepository) Update(ctx context.Context, license *entity.LicenseDB) (*entity.LicenseDB, error) { - if err := r.db.WithContext(ctx).Save(license).Error; err != nil { - logger.ContextLogger(ctx).Error("error when updating license", zap.Error(err)) - return nil, err - } - return license, nil -} - -func (r *LicenseRepository) FindByID(ctx context.Context, id string) (*entity.LicenseDB, error) { - licenseID, err := uuid.Parse(id) - if err != nil { - return nil, err - } - - license := new(entity.LicenseDB) - if err := r.db.WithContext(ctx).First(license, licenseID).Error; err != nil { - logger.ContextLogger(ctx).Error("error when finding license by ID", zap.Error(err)) - return nil, err - } - return license, nil -} - -func (r *LicenseRepository) GetAll(ctx context.Context, limit, offset int, statusFilter string) ([]*entity.LicenseGetAll, int64, error) { - var licenses []*entity.LicenseGetAll - var total int64 - - // Define the main query with status calculation and days to expire - subQuery := r.db.WithContext(ctx). - Table("licenses"). - Select(`licenses.*, partners.name as partner_name, - CASE - WHEN licenses.end_date < CURRENT_DATE THEN 'EXPIRED' - WHEN licenses.end_date < CURRENT_DATE + INTERVAL '30 days' THEN 'EXPIRING_SOON' - ELSE 'ACTIVE' - END as license_status, - (GREATEST(licenses.end_date - CURRENT_DATE, 0)) as days_to_expire, - users.name as created_by_name`). - Joins("LEFT JOIN partners ON licenses.partner_id = partners.id"). - Joins("LEFT JOIN users ON licenses.created_by = users.id") - - // Wrap the main query as a subquery to filter by status and apply pagination - query := r.db.Table("(?) as sub", subQuery) - if statusFilter != "" { - query = query.Where("license_status = ?", statusFilter) - } - - if limit > 0 { - query = query.Limit(limit) - } - if offset > 0 { - query = query.Offset(offset) - } - - if err := query.Find(&licenses).Error; err != nil { - return nil, 0, err - } - - // Get the total count of records matching the filter - countQuery := r.db.Table("(?) as sub", subQuery) - if statusFilter != "" { - countQuery = countQuery.Where("license_status = ?", statusFilter) - } - - if err := countQuery.Count(&total).Error; err != nil { - return nil, 0, err - } - - return licenses, total, nil -} - -func (r *LicenseRepository) FindByPartnerIDMaxEndDate(ctx context.Context, partnerID *int64) (*entity.LicenseDB, error) { - var licenseDB entity.LicenseDB - - today := time.Now().Format("2006-01-02") - - if err := r.db.WithContext(ctx). - Where("partner_id = ?", partnerID). - Where("start_date <= ?", today). - Where("end_date >= ?", today). - Order("end_date DESC"). - First(&licenseDB).Error; err != nil { - - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, nil - } - - return nil, err - } - - return &licenseDB, nil -} diff --git a/internal/repository/linkqu/linkqu.go b/internal/repository/linkqu/linkqu.go deleted file mode 100644 index dcad097..0000000 --- a/internal/repository/linkqu/linkqu.go +++ /dev/null @@ -1,304 +0,0 @@ -package linkqu - -import ( - "bytes" - "crypto/hmac" - "crypto/sha256" - "enaklo-pos-be/internal/entity" - "encoding/hex" - "encoding/json" - "fmt" - "io/ioutil" - "net/http" - "reflect" - "regexp" - "strings" - "time" -) - -type LinkQuConfig interface { - LinkQuBaseURL() string - LinkQuClientID() string - LinkQuClientSecret() string - LinkQuSignatureKey() string - LinkQuUsername() string - LinkQuPIN() string - LinkQuCallbackURL() string -} - -type LinkQuService struct { - config LinkQuConfig - client *http.Client -} - -type CreateQRISRequest struct { - Amount int64 `json:"amount"` - PartnerReff string `json:"partner_reff"` - CustomerID string `json:"customer_id"` - CustomerName string `json:"customer_name"` - Expired string `json:"expired"` - Username string `json:"username"` - Pin string `json:"pin"` - CustomerPhone string `json:"customer_phone"` - CustomerEmail string `json:"customer_email"` - Signature string `json:"signature"` - ClientID string `json:"client_id"` - URLCallback string `json:"url_callback"` -} - -type CreateVARequest struct { - Amount int64 `json:"amount"` - PartnerReff string `json:"partner_reff"` - CustomerID string `json:"customer_id"` - CustomerName string `json:"customer_name"` - Expired string `json:"expired"` - Username string `json:"username"` - Pin string `json:"pin"` - CustomerPhone string `json:"customer_phone"` - CustomerEmail string `json:"customer_email"` - Signature string `json:"signature"` - ClientID string `json:"client_id"` - URLCallback string `json:"url_callback"` - BankCode string `json:"bank_code"` -} - -func NewLinkQuService(config LinkQuConfig) *LinkQuService { - return &LinkQuService{ - config: config, - client: &http.Client{Timeout: 10 * time.Second}, - } -} - -func (s *LinkQuService) constructQRISPayload(req entity.LinkQuRequest) CreateQRISRequest { - return CreateQRISRequest{ - Amount: req.TotalAmount, - PartnerReff: req.PaymentReferenceID, - CustomerID: req.CustomerID, - CustomerName: req.CustomerName, - Expired: time.Now().Add(1 * time.Hour).Format("20060102150405"), - Username: s.config.LinkQuUsername(), - Pin: s.config.LinkQuPIN(), - CustomerPhone: req.CustomerPhone, - CustomerEmail: req.CustomerEmail, - ClientID: s.config.LinkQuClientID(), - URLCallback: s.config.LinkQuCallbackURL(), - } -} - -func (s *LinkQuService) constructVAPayload(req entity.LinkQuRequest) CreateVARequest { - return CreateVARequest{ - Amount: req.TotalAmount, - PartnerReff: req.PaymentReferenceID, - CustomerID: req.CustomerID, - CustomerName: req.CustomerName, - Expired: time.Now().Add(1 * time.Hour).Format("20060102150405"), - Username: s.config.LinkQuUsername(), - Pin: s.config.LinkQuPIN(), - CustomerPhone: req.CustomerPhone, - CustomerEmail: req.CustomerEmail, - ClientID: s.config.LinkQuClientID(), - URLCallback: s.config.LinkQuCallbackURL(), - BankCode: req.BankCode, - } -} - -func (s *LinkQuService) CreateQrisPayment(linkQuRequest entity.LinkQuRequest) (*entity.LinkQuQRISResponse, error) { - path := "/transaction/create/qris" - method := "POST" - - req := s.constructQRISPayload(linkQuRequest) - - if req.Expired == "" { - req.Expired = time.Now().Add(1 * time.Hour).Format("20060102150405") - } - - paramOrder := []string{"Amount", "Expired", "PartnerReff", "CustomerID", "CustomerName", "CustomerEmail", "ClientID"} - - signature, err := s.generateSignature(path, method, req, paramOrder) - if err != nil { - return nil, fmt.Errorf("failed to generate signature: %w", err) - } - req.Signature = signature - - reqBody, err := json.Marshal(req) - if err != nil { - return nil, fmt.Errorf("failed to marshal request body: %w", err) - } - - url := fmt.Sprintf("%s/%s%s", s.config.LinkQuBaseURL(), "linkqu-partner", path) - httpReq, err := http.NewRequest(method, url, bytes.NewBuffer(reqBody)) - if err != nil { - return nil, fmt.Errorf("failed to create HTTP request: %w", err) - } - - httpReq.Header.Set("Content-Type", "application/json") - httpReq.Header.Set("client-id", s.config.LinkQuClientID()) - httpReq.Header.Set("client-secret", s.config.LinkQuClientSecret()) - - resp, err := s.client.Do(httpReq) - if err != nil { - return nil, fmt.Errorf("failed to send request: %w", err) - } - defer resp.Body.Close() - - // Read response body - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - - // Check for non-200 status code - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body)) - } - - // Parse response - var qrisResp entity.LinkQuQRISResponse - if err := json.Unmarshal(body, &qrisResp); err != nil { - return nil, fmt.Errorf("failed to unmarshal response: %w", err) - } - - if qrisResp.ResponseCode != "00" { - return nil, fmt.Errorf("error when create qris linkqu, status code %s , %s", qrisResp.ResponseCode, qrisResp.ResponseDesc) - } - - return &qrisResp, nil -} - -func (s *LinkQuService) CreatePaymentVA(linkQuRequest entity.LinkQuRequest) (*entity.LinkQuPaymentVAResponse, error) { - path := "/transaction/create/va" - method := "POST" - - req := s.constructVAPayload(linkQuRequest) - - if req.Expired == "" { - req.Expired = time.Now().Add(1 * time.Hour).Format("20060102150405") - } - - paramOrder := []string{"Amount", "Expired", "BankCode", "PartnerReff", "CustomerID", "CustomerName", "CustomerEmail", "ClientID"} - - signature, err := s.generateSignature(path, method, req, paramOrder) - if err != nil { - return nil, fmt.Errorf("failed to generate signature: %w", err) - } - req.Signature = signature - - reqBody, err := json.Marshal(req) - if err != nil { - return nil, fmt.Errorf("failed to marshal request body: %w", err) - } - - url := fmt.Sprintf("%s/%s%s", s.config.LinkQuBaseURL(), "linkqu-partner", path) - httpReq, err := http.NewRequest(method, url, bytes.NewBuffer(reqBody)) - if err != nil { - return nil, fmt.Errorf("failed to create HTTP request: %w", err) - } - - httpReq.Header.Set("Content-Type", "application/json") - httpReq.Header.Set("client-id", s.config.LinkQuClientID()) - httpReq.Header.Set("client-secret", s.config.LinkQuClientSecret()) - - resp, err := s.client.Do(httpReq) - if err != nil { - return nil, fmt.Errorf("failed to send request: %w", err) - } - defer resp.Body.Close() - - // Read response body - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - - // Check for non-200 status code - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body)) - } - - // Parse response - var qrisResp entity.LinkQuPaymentVAResponse - if err := json.Unmarshal(body, &qrisResp); err != nil { - return nil, fmt.Errorf("failed to unmarshal response: %w", err) - } - - if qrisResp.ResponseCode != "00" { - return nil, fmt.Errorf("error when create qris linkqu, status code %s", qrisResp.ResponseCode) - } - - return &qrisResp, nil -} - -func (s *LinkQuService) generateSignature(path, method string, req interface{}, paramOrder []string) (string, error) { - var values []string - reqValue := reflect.ValueOf(req) - - for _, param := range paramOrder { - field := reqValue.FieldByNameFunc(func(fieldName string) bool { - return strings.EqualFold(fieldName, param) - }) - - if field.IsValid() { - values = append(values, fmt.Sprintf("%v", field.Interface())) - } else { - return "", fmt.Errorf("field %s not found in request struct", param) - } - } - - secondValue := strings.Join(values, "") - secondValue = cleanString(secondValue) - - signToString := path + method + secondValue - - h := hmac.New(sha256.New, []byte(s.config.LinkQuSignatureKey())) - h.Write([]byte(signToString)) - return hex.EncodeToString(h.Sum(nil)), nil -} - -func cleanString(s string) string { - reg := regexp.MustCompile("[^a-zA-Z0-9]+") - return strings.ToLower(reg.ReplaceAllString(s, "")) -} - -func (s *LinkQuService) CheckPaymentStatus(partnerReff string) (*entity.LinkQuCheckStatusResponse, error) { - path := "/transaction/payment/checkstatus" - method := "GET" - - url := fmt.Sprintf("%s%s%s?username=%s&partnerreff=%s", - s.config.LinkQuBaseURL(), "/linkqu-partner", path, s.config.LinkQuUsername(), partnerReff) - - httpReq, err := http.NewRequest(method, url, nil) - if err != nil { - return nil, fmt.Errorf("failed to create HTTP request: %w", err) - } - - httpReq.Header.Set("Content-Type", "application/json") - httpReq.Header.Set("client-id", s.config.LinkQuClientID()) - httpReq.Header.Set("client-secret", s.config.LinkQuClientSecret()) - - resp, err := s.client.Do(httpReq) - if err != nil { - return nil, fmt.Errorf("failed to send request: %w", err) - } - defer resp.Body.Close() - - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body)) - } - - // Parse response - var checkStatusResp entity.LinkQuCheckStatusResponse - if err := json.Unmarshal(body, &checkStatusResp); err != nil { - return nil, fmt.Errorf("failed to unmarshal response: %w", err) - } - - if checkStatusResp.ResponseCode != "00" { - return nil, fmt.Errorf("error when checking payment status, status code %s", checkStatusResp.ResponseCode) - } - - return &checkStatusResp, nil -} diff --git a/internal/repository/member_repo.go b/internal/repository/member_repo.go deleted file mode 100644 index 8866505..0000000 --- a/internal/repository/member_repo.go +++ /dev/null @@ -1,140 +0,0 @@ -package repository - -import ( - "enaklo-pos-be/internal/common/logger" - "enaklo-pos-be/internal/common/mycontext" - "enaklo-pos-be/internal/constants" - "enaklo-pos-be/internal/entity" - "enaklo-pos-be/internal/repository/models" - "errors" - "time" - - "go.uber.org/zap" - "gorm.io/gorm" -) - -type MemberRepository interface { - CreateRegistration(ctx mycontext.Context, registration *entity.MemberRegistration) (*entity.MemberRegistration, error) - GetRegistrationByToken(ctx mycontext.Context, token string) (*entity.MemberRegistration, error) - UpdateRegistrationStatus(ctx mycontext.Context, token string, status constants.RegistrationStatus) error - UpdateRegistrationOTP(ctx mycontext.Context, token string, otp string, expiresAt time.Time) error -} - -type memberRepository struct { - db *gorm.DB -} - -func NewMemberRepository(db *gorm.DB) MemberRepository { - return &memberRepository{ - db: db, - } -} - -func (r *memberRepository) CreateRegistration(ctx mycontext.Context, registration *entity.MemberRegistration) (*entity.MemberRegistration, error) { - registrationDB := r.toRegistrationDBModel(registration) - - if err := r.db.Create(®istrationDB).Error; err != nil { - logger.ContextLogger(ctx).Error("failed to create member registration", zap.Error(err)) - return nil, errors.New("failed to insert member registration") - } - - return registration, nil -} - -func (r *memberRepository) GetRegistrationByToken(ctx mycontext.Context, token string) (*entity.MemberRegistration, error) { - var registrationDB models.MemberRegistrationDB - - if err := r.db.Where("token = ?", token).First(®istrationDB).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, errors.New("registration not found") - } - logger.ContextLogger(ctx).Error("failed to get registration by token", zap.Error(err)) - return nil, errors.New("failed to get registration by token") - } - - registration := r.toDomainRegistrationModel(®istrationDB) - return registration, nil -} - -func (r *memberRepository) UpdateRegistrationStatus(ctx mycontext.Context, token string, status constants.RegistrationStatus) error { - now := time.Now() - - result := r.db.Model(&models.MemberRegistrationDB{}). - Where("token = ?", token). - Updates(map[string]interface{}{ - "status": status, - "updated_at": now, - }) - - if result.Error != nil { - logger.ContextLogger(ctx).Error("failed to update registration status", zap.Error(result.Error)) - return errors.New("failed to update registration status") - } - - if result.RowsAffected == 0 { - return errors.New("registration not found") - } - - return nil -} - -func (r *memberRepository) UpdateRegistrationOTP(ctx mycontext.Context, token string, otp string, expiresAt time.Time) error { - now := time.Now() - - result := r.db.Model(&models.MemberRegistrationDB{}). - Where("token = ?", token). - Updates(map[string]interface{}{ - "otp": otp, - "expires_at": expiresAt, - "updated_at": now, - }) - - if result.Error != nil { - logger.ContextLogger(ctx).Error("failed to update registration OTP", zap.Error(result.Error)) - return errors.New("failed to update registration OTP") - } - - if result.RowsAffected == 0 { - return errors.New("registration not found") - } - - return nil -} - -func (r *memberRepository) toRegistrationDBModel(registration *entity.MemberRegistration) models.MemberRegistrationDB { - return models.MemberRegistrationDB{ - ID: registration.ID, - Token: registration.Token, - Name: registration.Name, - Email: registration.Email, - Phone: registration.Phone, - BirthDate: registration.BirthDate, - OTP: registration.OTP, - Status: registration.Status.String(), - ExpiresAt: registration.ExpiresAt, - CreatedAt: registration.CreatedAt, - UpdatedAt: registration.UpdatedAt, - BranchID: registration.BranchID, - CashierID: registration.CashierID, - Password: registration.Password, - } -} - -func (r *memberRepository) toDomainRegistrationModel(dbModel *models.MemberRegistrationDB) *entity.MemberRegistration { - return &entity.MemberRegistration{ - ID: dbModel.ID, - Token: dbModel.Token, - Name: dbModel.Name, - Email: dbModel.Email, - Phone: dbModel.Phone, - BirthDate: dbModel.BirthDate, - OTP: dbModel.OTP, - Status: constants.RegistrationStatus(dbModel.Status), - ExpiresAt: dbModel.ExpiresAt, - CreatedAt: dbModel.CreatedAt, - UpdatedAt: dbModel.UpdatedAt, - BranchID: dbModel.BranchID, - CashierID: dbModel.CashierID, - Password: dbModel.Password, - } -} diff --git a/internal/repository/midtrans/init.go b/internal/repository/midtrans/init.go deleted file mode 100644 index fb6106f..0000000 --- a/internal/repository/midtrans/init.go +++ /dev/null @@ -1,126 +0,0 @@ -package mdtrns - -import ( - "enaklo-pos-be/internal/common/logger" - "enaklo-pos-be/internal/entity" - "fmt" - "log" - "strconv" - - "github.com/veritrans/go-midtrans" -) - -type MidtransConfig interface { - MidtransServerKey() string - MidtransClientKey() string - MidtranEnvType() int -} - -type ClientService struct { - client midtrans.Client - midtransConfig MidtransConfig -} - -func New(midtransConfig MidtransConfig) *ClientService { - midclient := midtrans.NewClient() - midclient.ServerKey = midtransConfig.MidtransServerKey() - midclient.ClientKey = midtransConfig.MidtransClientKey() - midclient.APIEnvType = midtrans.EnvironmentType(midtransConfig.MidtranEnvType()) - - return &ClientService{ - client: midclient, - midtransConfig: midtransConfig, - } -} - -func (c *ClientService) CreatePayment(order entity.MidtransRequest) (*entity.MidtransResponse, error) { - var snapGateway midtrans.SnapGateway - snapGateway = midtrans.SnapGateway{ - Client: c.client, - } - - var paymentMethod []midtrans.PaymentType - - if order.PaymentMethod == "QRIS" { - paymentMethod = []midtrans.PaymentType{midtrans.SourceGopay} - } - - snapReq := &midtrans.SnapReq{ - EnabledPayments: paymentMethod, - TransactionDetails: midtrans.TransactionDetails{ - OrderID: order.PaymentReferenceID, - GrossAmt: order.TotalAmount, - }, - } - - log.Println("GetToken:") - snapTokenResp, err := snapGateway.GetToken(snapReq) - - if err != nil { - logger.GetLogger().Error(fmt.Sprintf("error when create midtrans payment %v", err)) - return nil, err - } - - return &entity.MidtransResponse{ - Token: snapTokenResp.Token, - RedirectURL: snapTokenResp.RedirectURL, - }, nil - -} - -func (c ClientService) getProductItems(products []entity.OrderItem) []midtrans.ItemDetail { - var items []midtrans.ItemDetail - - for _, product := range products { - item := midtrans.ItemDetail{ - ID: strconv.FormatInt(product.ID, 10), - Name: product.ItemType, - Qty: int32(product.Quantity), - Price: int64(product.Price), - } - items = append(items, item) - } - - return items -} - -func (c *ClientService) CreateQrisPayment(order entity.MidtransRequest) (*entity.MidtransQrisResponse, error) { - coreGateway := midtrans.CoreGateway{ - Client: c.client, - } - - req := &midtrans.ChargeReq{ - PaymentType: midtrans.SourceGopay, - TransactionDetails: midtrans.TransactionDetails{ - OrderID: order.PaymentReferenceID, - GrossAmt: order.TotalAmount, - }, - } - - // Request charge and retrieve response - resp, err := coreGateway.Charge(req) - if err != nil { - logger.GetLogger().Error(fmt.Sprintf("error when creating QRIS payment: %v", err)) - return nil, err - } - - // Extract QR code URL from response actions - var qrCodeURL string - for _, action := range resp.Actions { - if action.Name == "generate-qr-code" { - qrCodeURL = action.URL - break - } - } - - if qrCodeURL == "" { - logger.GetLogger().Error("error: QR code URL not provided in response") - return nil, fmt.Errorf("QR code URL not provided in response") - } - - return &entity.MidtransQrisResponse{ - QrCodeUrl: qrCodeURL, - OrderID: order.PaymentReferenceID, - Amount: order.TotalAmount, - }, nil -} diff --git a/internal/repository/models/casheer_seasion.go b/internal/repository/models/casheer_seasion.go deleted file mode 100644 index fc72625..0000000 --- a/internal/repository/models/casheer_seasion.go +++ /dev/null @@ -1,21 +0,0 @@ -package models - -import "time" - -type CashierSessionDB struct { - ID int64 `gorm:"primaryKey"` - PartnerID int64 `gorm:"not null"` - CashierID int64 `gorm:"not null"` - OpenedAt time.Time `gorm:"not null"` - ClosedAt *time.Time - OpeningAmount float64 `gorm:"not null"` - ClosingAmount *float64 - ExpectedAmount *float64 - Notes *string - Status string `gorm:"not null"` - CreatedAt time.Time -} - -func (CashierSessionDB) TableName() string { - return "cashier_sessions" -} diff --git a/internal/repository/models/categories.go b/internal/repository/models/categories.go deleted file mode 100644 index ce2ebb8..0000000 --- a/internal/repository/models/categories.go +++ /dev/null @@ -1,16 +0,0 @@ -package models - -import "time" - -type CategoryDB struct { - ID int64 `gorm:"primaryKey;autoIncrement"` - PartnerID int64 `gorm:"not null"` - Name string `gorm:"type:varchar(255);not null"` - CreatedAt time.Time `gorm:"autoCreateTime"` - UpdatedAt time.Time `gorm:"autoUpdateTime"` - DeletedAt *time.Time -} - -func (CategoryDB) TableName() string { - return "categories" -} diff --git a/internal/repository/models/customer.go b/internal/repository/models/customer.go deleted file mode 100644 index e21e609..0000000 --- a/internal/repository/models/customer.go +++ /dev/null @@ -1,78 +0,0 @@ -package models - -import ( - "github.com/google/uuid" - "time" -) - -type CustomerDB struct { - ID int64 `gorm:"primaryKey;column:id"` - Name string `gorm:"column:name"` - Email string `gorm:"column:email"` - Phone string `gorm:"column:phone"` - Points int `gorm:"column:points"` - CreatedAt time.Time `gorm:"column:created_at"` - UpdatedAt time.Time `gorm:"column:updated_at"` - CustomerID string `gorm:"column:customer_id"` - BirthDate time.Time `gorm:"column:birth_date"` - Password string `gorm:"column:password"` - IsEmailVerified bool `gorm:"column:is_email_verified"` - IsPhoneVerified bool `gorm:"column:is_phone_verified"` -} - -func (CustomerDB) TableName() string { - return "customers" -} - -type PartnerMemberSequence struct { - ID int64 `gorm:"column:id;primary_key;auto_increment"` - PartnerID int64 `gorm:"column:partner_id;not null;index:idx_partner_month,unique"` - LastSequence int64 `gorm:"column:last_sequence;not null;default:0"` - UpdatedAt time.Time `gorm:"column:updated_at;not null"` -} - -func (PartnerMemberSequence) TableName() string { - return "partner_member_sequences" -} - -type CustomerPointsDB struct { - ID uint64 `gorm:"column:id;primaryKey;autoIncrement"` - CustomerID uint64 `gorm:"column:customer_id;not null"` - TotalPoints int `gorm:"column:total_points;not null;default:0"` - AvailablePoints int `gorm:"column:available_points;not null;default:0"` - LastUpdated time.Time `gorm:"column:last_updated;default:CURRENT_TIMESTAMP"` -} - -func (CustomerPointsDB) TableName() string { - return "customer_points" -} - -type CustomerPointTransactionDB struct { - ID uint64 `gorm:"column:id;primaryKey;autoIncrement"` - CustomerID int64 `gorm:"column:customer_id;not null"` - Reference string `gorm:"column:reference"` - PointsEarned int `gorm:"column:points_earned;not null"` - TransactionDate time.Time `gorm:"column:transaction_date;not null"` - ExpirationDate *time.Time `gorm:"column:expiration_date"` - Status string `gorm:"column:status;default:active"` - CreatedAt time.Time `gorm:"column:created_at;default:CURRENT_TIMESTAMP"` -} - -func (CustomerPointTransactionDB) TableName() string { - return "customer_point_transactions" -} - -type CustomerVerificationCodeDB struct { - ID uint64 `gorm:"column:id;primaryKey;autoIncrement"` - CustomerID uint64 `gorm:"column:customer_id;not null"` - Code string `gorm:"column:code;not null"` - Type string `gorm:"column:type;not null"` - ExpiresAt time.Time `gorm:"column:expires_at;not null"` - IsUsed bool `gorm:"column:is_used;default:false"` - CreatedAt time.Time `gorm:"column:created_at;default:CURRENT_TIMESTAMP"` - VerificationID uuid.UUID `gorm:"column:verification_id;type:uuid;default:uuid_generate_v4()"` -} - -func (CustomerVerificationCodeDB) TableName() string { - return "customer_verification_codes" -} diff --git a/internal/repository/models/in_progress_order.go b/internal/repository/models/in_progress_order.go deleted file mode 100644 index a78b19e..0000000 --- a/internal/repository/models/in_progress_order.go +++ /dev/null @@ -1,35 +0,0 @@ -package models - -import "time" - -type InProgressOrderDB struct { - ID string `gorm:"primaryKey;column:id"` - PartnerID int64 `gorm:"column:partner_id"` - CustomerID *int64 `gorm:"column:customer_id"` - CustomerName string `gorm:"column:customer_name"` - PaymentType string `gorm:"column:payment_type"` - CreatedBy int64 `gorm:"column:created_by"` - CreatedAt time.Time `gorm:"column:created_at"` - UpdatedAt time.Time `gorm:"column:updated_at"` - TableNumber string `gorm:"column:table_number"` - OrderItems []InProgressOrderItemDB `gorm:"foreignKey:InProgressOrderIO"` - OrderType string `gorm:"column:order_type"` -} - -type InProgressOrderItemDB struct { - ID int64 `gorm:"primaryKey;column:id"` - InProgressOrderIO string `gorm:"column:in_progress_order_id"` - ItemID int64 `gorm:"column:item_id"` - Quantity int `gorm:"column:quantity"` - CreatedBy int64 `gorm:"column:created_by"` - CreatedAt time.Time `gorm:"column:created_at"` - Product ProductDB `gorm:"foreignKey:ItemID;references:ID"` -} - -func (InProgressOrderItemDB) TableName() string { - return "in_progress_order_items" -} - -func (InProgressOrderDB) TableName() string { - return "in_progress_order" -} diff --git a/internal/repository/models/member.go b/internal/repository/models/member.go deleted file mode 100644 index 82d0d1e..0000000 --- a/internal/repository/models/member.go +++ /dev/null @@ -1,26 +0,0 @@ -package models - -import ( - "time" -) - -type MemberRegistrationDB struct { - ID string `gorm:"column:id;primary_key"` - Token string `gorm:"column:token;unique_index"` - Name string `gorm:"column:name"` - Email string `gorm:"column:email"` - Phone string `gorm:"column:phone"` - BirthDate time.Time `gorm:"column:birth_date"` - OTP string `gorm:"column:otp"` - Status string `gorm:"column:status"` - ExpiresAt time.Time `gorm:"column:expires_at"` - CreatedAt time.Time `gorm:"column:created_at"` - UpdatedAt time.Time `gorm:"column:updated_at"` - BranchID int64 `gorm:"column:branch_id"` - CashierID int64 `gorm:"column:cashier_id"` - Password string `gorm:"column:password"` -} - -func (MemberRegistrationDB) TableName() string { - return "member_registrations" -} diff --git a/internal/repository/models/order.go b/internal/repository/models/order.go deleted file mode 100644 index 189efe6..0000000 --- a/internal/repository/models/order.go +++ /dev/null @@ -1,96 +0,0 @@ -package models - -import ( - "time" -) - -type OrderDB struct { - ID int64 `gorm:"primaryKey;column:id"` - PartnerID int64 `gorm:"column:partner_id"` - CustomerID *int64 `gorm:"column:customer_id"` - InquiryID *string `gorm:"column:inquiry_id"` - Status string `gorm:"column:status"` - Amount float64 `gorm:"column:amount"` - Tax float64 `gorm:"column:tax"` - Total float64 `gorm:"column:total"` - PaymentType string `gorm:"column:payment_type"` - Source string `gorm:"column:source"` - CreatedBy int64 `gorm:"column:created_by"` - CreatedAt time.Time `gorm:"column:created_at"` - UpdatedAt time.Time `gorm:"column:updated_at"` - OrderItems []OrderItemDB `gorm:"foreignKey:OrderID"` - OrderType string `gorm:"column:order_type"` - TableNumber string `gorm:"column:table_number"` - PaymentProvider string `gorm:"column:payment_provider"` - CustomerName string `gorm:"column:customer_name"` - CashierSessionID int64 `gorm:"column:cashier_session_id"` - Description string `gorm:"column:description"` -} - -func (OrderDB) TableName() string { - return "orders" -} - -type OrderItemDB struct { - ID int64 `gorm:"primaryKey;column:order_item_id"` - OrderID int64 `gorm:"column:order_id"` - ItemID int64 `gorm:"column:item_id"` - ItemName string `gorm:"column:item_name"` - ItemType string `gorm:"column:item_type"` - Price float64 `gorm:"column:price"` - Quantity int `gorm:"column:quantity"` - Status string `gorm:"column:status;default:ACTIVE"` - CreatedBy int64 `gorm:"column:created_by"` - CreatedAt time.Time `gorm:"column:created_at"` - Product ProductDB `gorm:"foreignKey:ItemID;references:ID"` - Notes string `gorm:"column:notes"` -} - -func (OrderItemDB) TableName() string { - return "order_items" -} - -type OrderInquiryDB struct { - ID string `gorm:"primaryKey;column:id"` - PartnerID int64 `gorm:"column:partner_id"` - CustomerID *int64 `gorm:"column:customer_id"` - CustomerName string `gorm:"column:customer_name"` - CustomerEmail string `gorm:"column:customer_email"` - CustomerPhoneNumber string `gorm:"column:customer_phone_number"` - Status string `gorm:"column:status"` - Amount float64 `gorm:"column:amount"` - Tax float64 `gorm:"column:tax"` - Total float64 `gorm:"column:total"` - PaymentType string `gorm:"column:payment_type"` - Source string `gorm:"column:source"` - CreatedBy int64 `gorm:"column:created_by"` - CreatedAt time.Time `gorm:"column:created_at"` - UpdatedAt time.Time `gorm:"column:updated_at"` - ExpiresAt time.Time `gorm:"column:expires_at"` - InquiryItems []InquiryItemDB `gorm:"foreignKey:InquiryID"` - PaymentProvider string `gorm:"column:payment_provider"` - TableNumber string `gorm:"column:table_number"` - OrderType string `gorm:"column:order_type"` - CashierSessionID int64 `gorm:"column:cashier_session_id"` -} - -func (OrderInquiryDB) TableName() string { - return "order_inquiries" -} - -type InquiryItemDB struct { - ID int64 `gorm:"primaryKey;column:id"` - InquiryID string `gorm:"column:inquiry_id"` - ItemID int64 `gorm:"column:item_id"` - ItemType string `gorm:"column:item_type"` - ItemName string `gorm:"column:item_name"` - Price float64 `gorm:"column:price"` - Quantity int `gorm:"column:quantity"` - CreatedBy int64 `gorm:"column:created_by"` - CreatedAt time.Time `gorm:"column:created_at"` - Notes string `gorm:"column:notes"` -} - -func (InquiryItemDB) TableName() string { - return "inquiry_items" -} diff --git a/internal/repository/models/partner_setting.go b/internal/repository/models/partner_setting.go deleted file mode 100644 index 112d23c..0000000 --- a/internal/repository/models/partner_setting.go +++ /dev/null @@ -1,53 +0,0 @@ -package models - -import ( - "time" -) - -type PartnerSettingsDB struct { - PartnerID int64 `gorm:"primaryKey;column:partner_id"` - TaxEnabled bool `gorm:"column:tax_enabled;default:false"` - TaxPercentage float64 `gorm:"column:tax_percentage;default:10.00"` - InvoicePrefix string `gorm:"column:invoice_prefix;default:INV"` - BusinessHours string `gorm:"column:business_hours;type:json"` // JSON string - LogoURL string `gorm:"column:logo_url"` - ThemeColor string `gorm:"column:theme_color;default:#000000"` - ReceiptFooterText string `gorm:"column:receipt_footer_text;type:text"` - ReceiptHeaderText string `gorm:"column:receipt_header_text;type:text"` - CreatedAt time.Time `gorm:"column:created_at;default:CURRENT_TIMESTAMP"` - UpdatedAt time.Time `gorm:"column:updated_at;default:CURRENT_TIMESTAMP"` -} - -func (PartnerSettingsDB) TableName() string { - return "partner_settings" -} - -type PartnerPaymentMethodDB struct { - ID int64 `gorm:"primaryKey;column:id;autoIncrement"` - PartnerID int64 `gorm:"column:partner_id;index:idx_partner_payment"` - PaymentMethod string `gorm:"column:payment_method;index:idx_partner_payment"` - IsEnabled bool `gorm:"column:is_enabled;default:true"` - DisplayName string `gorm:"column:display_name"` - DisplayOrder int `gorm:"column:display_order;default:0"` - AdditionalInfo string `gorm:"column:additional_info;type:json"` // JSON string - CreatedAt time.Time `gorm:"column:created_at;default:CURRENT_TIMESTAMP"` - UpdatedAt time.Time `gorm:"column:updated_at;default:CURRENT_TIMESTAMP"` -} - -func (PartnerPaymentMethodDB) TableName() string { - return "partner_payment_methods" -} - -type PartnerFeatureFlagDB struct { - ID int64 `gorm:"primaryKey;column:id;autoIncrement"` - PartnerID int64 `gorm:"column:partner_id;index:idx_partner_feature"` - FeatureKey string `gorm:"column:feature_key;index:idx_partner_feature"` - IsEnabled bool `gorm:"column:is_enabled;default:true"` - Config string `gorm:"column:config;type:json"` // JSON string - CreatedAt time.Time `gorm:"column:created_at;default:CURRENT_TIMESTAMP"` - UpdatedAt time.Time `gorm:"column:updated_at;default:CURRENT_TIMESTAMP"` -} - -func (PartnerFeatureFlagDB) TableName() string { - return "partner_feature_flags" -} diff --git a/internal/repository/models/product.go b/internal/repository/models/product.go deleted file mode 100644 index 0164eb1..0000000 --- a/internal/repository/models/product.go +++ /dev/null @@ -1,23 +0,0 @@ -package models - -import ( - "time" -) - -type ProductDB struct { - ID int64 `gorm:"primaryKey;column:id"` - SiteID int64 `gorm:"column:site_id"` - PartnerID int64 `gorm:"column:partner_id"` - Name string `gorm:"column:name"` - Description string `gorm:"column:description"` - Price float64 `gorm:"column:price"` - Type string `gorm:"column:type"` - Status string `gorm:"column:status"` - CreatedAt time.Time `gorm:"column:created_at"` - UpdatedAt time.Time `gorm:"column:updated_at"` - Image string `gorm:"column:image"` -} - -func (ProductDB) TableName() string { - return "products" -} diff --git a/internal/repository/models/transaction.go b/internal/repository/models/transaction.go deleted file mode 100644 index 2d45fa0..0000000 --- a/internal/repository/models/transaction.go +++ /dev/null @@ -1,21 +0,0 @@ -package models - -import ( - "time" -) - -type TransactionDB struct { - ID string `gorm:"type:uuid;default:gen_random_uuid();primaryKey;column:id"` - OrderID int64 `gorm:"column:order_id"` - Amount float64 `gorm:"column:amount"` - PaymentMethod string `gorm:"column:payment_method"` - Status string `gorm:"column:status"` - CreatedAt time.Time `gorm:"column:created_at"` - UpdatedAt time.Time `gorm:"column:updated_at"` - PartnerID int64 `gorm:"column:partner_id"` - TransactionType string `gorm:"column:transaction_type"` -} - -func (TransactionDB) TableName() string { - return "transactions" -} diff --git a/internal/repository/orde_repo.go b/internal/repository/orde_repo.go deleted file mode 100644 index 54187f2..0000000 --- a/internal/repository/orde_repo.go +++ /dev/null @@ -1,1073 +0,0 @@ -package repository - -import ( - "enaklo-pos-be/internal/common/logger" - "enaklo-pos-be/internal/common/mycontext" - "enaklo-pos-be/internal/entity" - "enaklo-pos-be/internal/repository/models" - "time" - - "github.com/pkg/errors" - "go.uber.org/zap" - "gorm.io/gorm" -) - -type OrderRepository interface { - Create(ctx mycontext.Context, order *entity.Order) (*entity.Order, error) - FindByID(ctx mycontext.Context, id int64) (*entity.Order, error) - CreateInquiry(ctx mycontext.Context, inquiry *entity.OrderInquiry) (*entity.OrderInquiry, error) - FindInquiryByID(ctx mycontext.Context, id string) (*entity.OrderInquiry, error) - UpdateInquiryStatus(ctx mycontext.Context, id string, status string) error - GetOrderHistoryByPartnerID(ctx mycontext.Context, partnerID *int64, req entity.SearchRequest) ([]*entity.Order, int64, error) - CreateOrder(ctx mycontext.Context, order *entity.Order, tx *gorm.DB) (*entity.Order, error) - CreateOrderItems(ctx mycontext.Context, orderID int64, items []entity.OrderItem, tx *gorm.DB) error - CreateOrderItem(ctx mycontext.Context, orderID int64, item *entity.OrderItem) error - DeleteOrderItem(ctx mycontext.Context, orderItemID int64) error - GetListByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int, status string) ([]*entity.Order, error) - GetOrderPaymentMethodBreakdown(ctx mycontext.Context, partnerID int64, req entity.SearchRequest) ([]entity.PaymentMethodBreakdown, error) - GetRevenueOverview(ctx mycontext.Context, req entity.RevenueOverviewRequest) ([]entity.RevenueOverviewItem, error) - GetSalesByCategory(ctx mycontext.Context, req entity.SalesByCategoryRequest) ([]entity.SalesByCategoryItem, error) - GetPopularProducts(ctx mycontext.Context, req entity.PopularProductsRequest) ([]entity.PopularProductItem, error) - FindByIDAndPartnerID(ctx mycontext.Context, id int64, partnerID int64) (*entity.Order, error) - GetOrderHistoryByUserID(ctx mycontext.Context, userID int64, req entity.SearchRequest) ([]*entity.Order, int64, error) - FindByIDAndCustomerID(ctx mycontext.Context, id int64, customerID int64) (*entity.Order, error) - UpdateOrder(ctx mycontext.Context, id int64, status string, description string) error - UpdateOrderItem(ctx mycontext.Context, orderItemID int64, quantity int) error - UpdateOrderTotals(ctx mycontext.Context, orderID int64, amount, tax, total float64) error - UpdateOrderTotalsWithTx(ctx mycontext.Context, trx *gorm.DB, orderID int64, amount, tax, total float64) error - FindByIDWithTx(ctx mycontext.Context, id int64, tx *gorm.DB) (*entity.Order, error) -} - -type orderRepository struct { - db *gorm.DB -} - -func NeworderRepository(db *gorm.DB) *orderRepository { - return &orderRepository{db: db} -} - -func (r *orderRepository) Create(ctx mycontext.Context, order *entity.Order) (*entity.Order, error) { - orderDB := r.toOrderDBModel(order) - - tx := r.db.Begin() - if tx.Error != nil { - return nil, errors.Wrap(tx.Error, "failed to begin transaction") - } - defer func() { - if r := recover(); r != nil { - tx.Rollback() - } - }() - - if order.InProgressOrderID != 0 { - orderDB.ID = order.InProgressOrderID - - if err := tx.Omit("customer_id", "partner_id", "customer_name", "created_by").Save(&orderDB).Error; err != nil { - tx.Rollback() - return nil, errors.Wrap(err, "failed to update in-progress order") - } - - order.ID = order.InProgressOrderID - - if err := tx.Where("order_id = ?", order.ID).Delete(&models.OrderItemDB{}).Error; err != nil { - tx.Rollback() - return nil, errors.Wrap(err, "failed to delete existing order items") - } - } else { - if err := tx.Create(&orderDB).Error; err != nil { - tx.Rollback() - return nil, errors.Wrap(err, "failed to insert order") - } - - order.ID = orderDB.ID - } - - for i := range order.OrderItems { - item := &order.OrderItems[i] - item.OrderID = order.ID - - itemDB := r.toOrderItemDBModel(item) - - if err := tx.Create(&itemDB).Error; err != nil { - tx.Rollback() - return nil, errors.Wrap(err, "failed to insert order item") - } - - item.ID = itemDB.ID - } - - if err := tx.Commit().Error; err != nil { - return nil, errors.Wrap(err, "failed to commit transaction") - } - - var updatedOrderDB models.OrderDB - if err := r.db.Preload("OrderItems").First(&updatedOrderDB, order.ID).Error; err != nil { - return nil, errors.Wrap(err, "failed to fetch updated order") - } - - updatedOrder := r.toDomainOrderModel(&updatedOrderDB) - - return updatedOrder, nil -} - -func (r *orderRepository) FindByID(ctx mycontext.Context, id int64) (*entity.Order, error) { - var orderDB models.OrderDB - - if err := r.db.Preload("OrderItems").First(&orderDB, id).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, errors.New("order not found") - } - return nil, errors.Wrap(err, "failed to find order") - } - - order := r.toDomainOrderModel(&orderDB) - - return order, nil -} - -func (r *orderRepository) FindByIDWithTx(ctx mycontext.Context, id int64, tx *gorm.DB) (*entity.Order, error) { - var orderDB models.OrderDB - - if err := tx.Preload("OrderItems").First(&orderDB, id).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, errors.New("order not found") - } - return nil, errors.Wrap(err, "failed to find order in transaction") - } - - order := r.toDomainOrderModel(&orderDB) - return order, nil -} - -func (r *orderRepository) CreateInquiry(ctx mycontext.Context, inquiry *entity.OrderInquiry) (*entity.OrderInquiry, error) { - inquiryDB := r.toOrderInquiryDBModel(inquiry) - inquiryItems := make([]models.InquiryItemDB, 0, len(inquiry.OrderItems)) - - for _, item := range inquiry.OrderItems { - inquiryItems = append(inquiryItems, models.InquiryItemDB{ - InquiryID: inquiryDB.ID, - ItemID: item.ItemID, - ItemType: item.ItemType, - ItemName: item.ItemName, - Price: item.Price, - Quantity: item.Quantity, - CreatedBy: item.CreatedBy, - CreatedAt: time.Now(), - Notes: item.Notes, - }) - } - - tx := r.db.Begin() - if tx.Error != nil { - return nil, errors.Wrap(tx.Error, "failed to begin transaction") - } - defer func() { - if r := recover(); r != nil { - tx.Rollback() - } - }() - - if err := tx.Create(&inquiryDB).Error; err != nil { - tx.Rollback() - return nil, errors.Wrap(err, "failed to insert order inquiry") - } - - if len(inquiryItems) > 0 { - if err := tx.CreateInBatches(inquiryItems, 100).Error; err != nil { - tx.Rollback() - return nil, errors.Wrap(err, "failed to insert inquiry items") - } - } - - if err := tx.Commit().Error; err != nil { - return nil, errors.Wrap(err, "failed to commit transaction") - } - - return inquiry, nil -} - -func (r *orderRepository) FindInquiryByID(ctx mycontext.Context, id string) (*entity.OrderInquiry, error) { - var inquiryDB models.OrderInquiryDB - - if err := r.db.Preload("InquiryItems").First(&inquiryDB, "id = ?", id).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, errors.New("inquiry not found") - } - return nil, errors.Wrap(err, "failed to find inquiry") - } - - inquiry := r.toDomainOrderInquiryModel(&inquiryDB) - - orderItems := make([]entity.OrderItem, 0, len(inquiryDB.InquiryItems)) - for _, itemDB := range inquiryDB.InquiryItems { - orderItems = append(orderItems, entity.OrderItem{ - ItemID: itemDB.ItemID, - ItemType: itemDB.ItemType, - ItemName: itemDB.ItemName, - Price: itemDB.Price, - Quantity: itemDB.Quantity, - CreatedBy: itemDB.CreatedBy, - CreatedAt: itemDB.CreatedAt, - Notes: itemDB.Notes, - }) - } - inquiry.OrderItems = orderItems - - return inquiry, nil -} - -func (r *orderRepository) UpdateInquiryStatus(ctx mycontext.Context, id string, status string) error { - now := time.Now() - - result := r.db.Model(&models.OrderInquiryDB{}). - Where("id = ?", id). - Updates(map[string]interface{}{ - "status": status, - "updated_at": now, - }) - - if result.Error != nil { - return errors.Wrap(result.Error, "failed to update inquiry status") - } - - if result.RowsAffected == 0 { - logger.ContextLogger(ctx).Warn("no inquiry updated", zap.String("id", id)) - } - - return nil -} - -func (r *orderRepository) UpdateOrder(ctx mycontext.Context, id int64, status string, description string) error { - now := time.Now() - - result := r.db.Model(&models.OrderDB{}). - Where("id = ?", id). - Updates(map[string]interface{}{ - "status": status, - "updated_at": now, - "description": description, - }) - - if result.Error != nil { - return errors.Wrap(result.Error, "failed to update order status") - } - - if result.RowsAffected == 0 { - logger.ContextLogger(ctx).Warn("no order updated") - } - - return nil -} - -func (r *orderRepository) toOrderDBModel(order *entity.Order) models.OrderDB { - return models.OrderDB{ - ID: order.ID, - PartnerID: order.PartnerID, - CustomerID: order.CustomerID, - InquiryID: order.InquiryID, - Status: order.Status, - Amount: order.Amount, - Tax: order.Tax, - Total: order.Total, - PaymentType: order.PaymentType, - Source: order.Source, - CreatedBy: order.CreatedBy, - CreatedAt: order.CreatedAt, - UpdatedAt: order.UpdatedAt, - OrderType: order.OrderType, - TableNumber: order.TableNumber, - PaymentProvider: order.PaymentProvider, - CustomerName: order.CustomerName, - CashierSessionID: order.CashierSessionID, - } -} - -func (r *orderRepository) toDomainOrderModel(dbModel *models.OrderDB) *entity.Order { - orderItems := make([]entity.OrderItem, 0, len(dbModel.OrderItems)) - for _, itemDB := range dbModel.OrderItems { - orderItems = append(orderItems, entity.OrderItem{ - ID: itemDB.ID, - ItemID: itemDB.ItemID, - ItemType: itemDB.ItemType, - ItemName: itemDB.ItemName, - Price: itemDB.Price, - Quantity: itemDB.Quantity, - Status: itemDB.Status, - CreatedBy: itemDB.CreatedBy, - CreatedAt: itemDB.CreatedAt, - Notes: itemDB.Notes, - }) - } - - return &entity.Order{ - ID: dbModel.ID, - PartnerID: dbModel.PartnerID, - CustomerID: dbModel.CustomerID, - InquiryID: dbModel.InquiryID, - Status: dbModel.Status, - Amount: dbModel.Amount, - Tax: dbModel.Tax, - Total: dbModel.Total, - PaymentType: dbModel.PaymentType, - Source: dbModel.Source, - CreatedBy: dbModel.CreatedBy, - CreatedAt: dbModel.CreatedAt, - UpdatedAt: dbModel.UpdatedAt, - OrderItems: orderItems, - CustomerName: dbModel.CustomerName, - TableNumber: dbModel.TableNumber, - OrderType: dbModel.OrderType, - PaymentProvider: dbModel.PaymentProvider, - } -} - -func (r *orderRepository) toOrderItemDBModel(item *entity.OrderItem) models.OrderItemDB { - return models.OrderItemDB{ - ID: item.ID, - OrderID: item.OrderID, - ItemID: item.ItemID, - ItemType: item.ItemType, - ItemName: item.ItemName, - Price: item.Price, - Quantity: item.Quantity, - Status: item.Status, - CreatedBy: item.CreatedBy, - CreatedAt: item.CreatedAt, - Notes: item.Notes, - } -} - -func (r *orderRepository) toDomainOrderItemModel(dbModel *models.OrderItemDB) *entity.OrderItem { - return &entity.OrderItem{ - ID: dbModel.ID, - OrderID: dbModel.OrderID, - ItemID: dbModel.ItemID, - ItemType: dbModel.ItemType, - Price: dbModel.Price, - Quantity: dbModel.Quantity, - Status: dbModel.Status, - CreatedBy: dbModel.CreatedBy, - CreatedAt: dbModel.CreatedAt, - ItemName: dbModel.ItemName, - Notes: dbModel.Notes, - Product: &entity.Product{ - ID: dbModel.ItemID, - Name: dbModel.ItemName, - }, - } -} - -func (r *orderRepository) toOrderInquiryDBModel(inquiry *entity.OrderInquiry) models.OrderInquiryDB { - return models.OrderInquiryDB{ - ID: inquiry.ID, - PartnerID: inquiry.PartnerID, - CustomerID: &inquiry.CustomerID, - Status: inquiry.Status, - Amount: inquiry.Amount, - Tax: inquiry.Tax, - Total: inquiry.Total, - PaymentType: inquiry.PaymentType, - Source: inquiry.Source, - CreatedBy: inquiry.CreatedBy, - CreatedAt: inquiry.CreatedAt, - UpdatedAt: inquiry.UpdatedAt, - ExpiresAt: inquiry.ExpiresAt, - CustomerName: inquiry.CustomerName, - CustomerPhoneNumber: inquiry.CustomerPhoneNumber, - CustomerEmail: inquiry.CustomerEmail, - PaymentProvider: inquiry.PaymentProvider, - OrderType: inquiry.OrderType, - TableNumber: inquiry.TableNumber, - CashierSessionID: inquiry.CashierSessionID, - } -} - -func (r *orderRepository) toDomainOrderInquiryModel(dbModel *models.OrderInquiryDB) *entity.OrderInquiry { - inquiry := &entity.OrderInquiry{ - ID: dbModel.ID, - PartnerID: dbModel.PartnerID, - Status: dbModel.Status, - Amount: dbModel.Amount, - Tax: dbModel.Tax, - Total: dbModel.Total, - PaymentType: dbModel.PaymentType, - Source: dbModel.Source, - CreatedBy: dbModel.CreatedBy, - CreatedAt: dbModel.CreatedAt, - ExpiresAt: dbModel.ExpiresAt, - OrderItems: []entity.OrderItem{}, - OrderType: dbModel.OrderType, - CustomerName: dbModel.CustomerName, - PaymentProvider: dbModel.PaymentProvider, - TableNumber: dbModel.TableNumber, - CashierSessionID: dbModel.CashierSessionID, - } - - if dbModel.CustomerID != nil { - inquiry.CustomerID = *dbModel.CustomerID - } - - inquiry.UpdatedAt = dbModel.UpdatedAt - - return inquiry -} - -func (r *orderRepository) GetOrderHistoryByPartnerID(ctx mycontext.Context, partnerID *int64, req entity.SearchRequest) ([]*entity.Order, int64, error) { - queryBuilder := NewQueryBuilder[models.OrderDB](r.db) - filters := []Filter{ - Equal("partner_id", partnerID), - } - - if req.Status != "" { - if req.Status == "PAID" { - filters = append(filters, In("status", []string{"PAID", "REFUNDED"})) - } else { - filters = append(filters, Equal("status", req.Status)) - } - } - - if !req.Start.IsZero() { - filters = append(filters, GreaterEqual("created_at", req.Start)) - } - - if !req.End.IsZero() { - filters = append(filters, LessEqual("created_at", req.End)) - } - - options := QueryOptions{ - Filters: filters, - Limit: req.Limit, - Offset: req.Offset, - OrderBy: []string{"created_at DESC"}, - Preloads: []string{"OrderItems"}, - } - - baseQuery := queryBuilder.BuildQuery(options) - totalCount, err := queryBuilder.Count(baseQuery) - if err != nil { - return nil, 0, err - } - - query := queryBuilder.ExecuteQuery(baseQuery, options) - ordersDB, err := queryBuilder.Find(query) - if err != nil { - return nil, 0, err - } - - orders := r.convertOrdersToEntity(ordersDB) - - return orders, totalCount, nil -} - -func (r *orderRepository) convertOrdersToEntity(ordersDB []models.OrderDB) []*entity.Order { - orders := make([]*entity.Order, 0, len(ordersDB)) - - for _, orderDB := range ordersDB { - order := r.toDomainOrderModel(&orderDB) - order.OrderItems = r.convertOrderItemsToEntity(orderDB.OrderItems) - orders = append(orders, order) - } - - return orders -} - -func (r *orderRepository) convertOrderItemsToEntity(itemsDB []models.OrderItemDB) []entity.OrderItem { - items := make([]entity.OrderItem, 0, len(itemsDB)) - - for _, itemDB := range itemsDB { - item := r.toDomainOrderItemModel(&itemDB) - items = append(items, *item) - } - - return items -} - -func (r *orderRepository) GetOrderHistoryByUserID(ctx mycontext.Context, userID int64, req entity.SearchRequest) ([]*entity.Order, int64, error) { - var ordersDB []models.OrderDB - var totalCount int64 - - baseQuery := r.db.Model(&models.OrderDB{}).Where("customer_id = ?", userID) - - if req.Status != "" { - baseQuery = baseQuery.Where("status = ?", req.Status) - } - - if !req.Start.IsZero() { - baseQuery = baseQuery.Where("created_at >= ?", req.Start) - } - - if !req.End.IsZero() { - baseQuery = baseQuery.Where("created_at <= ?", req.End) - } - - if err := baseQuery.Count(&totalCount).Error; err != nil { - return nil, 0, errors.Wrap(err, "failed to count total orders") - } - - query := baseQuery.Session(&gorm.Session{}) - - query = query.Order("created_at DESC") - - if req.Limit > 0 { - query = query.Limit(req.Limit) - } - - if req.Offset > 0 { - query = query.Offset(req.Offset) - } - - if err := query.Preload("OrderItems").Find(&ordersDB).Error; err != nil { - return nil, 0, errors.Wrap(err, "failed to find order history by partner ID") - } - - orders := make([]*entity.Order, 0, len(ordersDB)) - for _, orderDB := range ordersDB { - order := r.toDomainOrderModel(&orderDB) - order.OrderItems = make([]entity.OrderItem, 0, len(orderDB.OrderItems)) - - for _, itemDB := range orderDB.OrderItems { - item := r.toDomainOrderItemModel(&itemDB) - order.OrderItems = append(order.OrderItems, *item) - } - - orders = append(orders, order) - } - - return orders, totalCount, nil -} - -func (r *orderRepository) CreateOrder(ctx mycontext.Context, order *entity.Order, tx *gorm.DB) (*entity.Order, error) { - orderDB := r.toOrderDBModel(order) - - // Use provided transaction or create new one - var dbTx *gorm.DB - if tx != nil { - dbTx = tx - } else { - dbTx = r.db.Begin() - if dbTx.Error != nil { - return nil, errors.Wrap(dbTx.Error, "failed to begin transaction") - } - defer func() { - if r := recover(); r != nil { - dbTx.Rollback() - } - }() - } - - if order.InProgressOrderID != 0 { - // Update existing order - orderDB.ID = order.InProgressOrderID - if err := dbTx.Omit("customer_id", "partner_id", "customer_name", "created_by").Save(&orderDB).Error; err != nil { - if tx == nil { - dbTx.Rollback() - } - return nil, errors.Wrap(err, "failed to update in-progress order") - } - order.ID = order.InProgressOrderID - } else { - // Create new order - if err := dbTx.Create(&orderDB).Error; err != nil { - if tx == nil { - dbTx.Rollback() - } - return nil, errors.Wrap(err, "failed to insert order") - } - order.ID = orderDB.ID - } - - // Only commit if we created the transaction - if tx == nil { - if err := dbTx.Commit().Error; err != nil { - return nil, errors.Wrap(err, "failed to commit transaction") - } - } - - // Return the order with the ID set, but without items (items will be added separately) - return order, nil -} - -func (r *orderRepository) CreateOrderItems(ctx mycontext.Context, orderID int64, items []entity.OrderItem, tx *gorm.DB) error { - // Use provided transaction or create new one - var dbTx *gorm.DB - if tx != nil { - dbTx = tx - } else { - dbTx = r.db.Begin() - if dbTx.Error != nil { - return errors.Wrap(dbTx.Error, "failed to begin transaction") - } - defer func() { - if r := recover(); r != nil { - dbTx.Rollback() - } - }() - } - - for _, item := range items { - itemDB := r.toOrderItemDBModel(&item) - itemDB.OrderID = orderID - - if err := dbTx.Create(&itemDB).Error; err != nil { - if tx == nil { - dbTx.Rollback() - } - return errors.Wrap(err, "failed to insert order item") - } - - item.ID = itemDB.ID - } - - // Only commit if we created the transaction - if tx == nil { - if err := dbTx.Commit().Error; err != nil { - return errors.Wrap(err, "failed to commit transaction") - } - } - - return nil -} - -func (r *orderRepository) CreateOrderItem(ctx mycontext.Context, orderID int64, item *entity.OrderItem) error { - itemDB := r.toOrderItemDBModel(item) - itemDB.OrderID = orderID - - if err := r.db.Create(&itemDB).Error; err != nil { - return errors.Wrap(err, "failed to insert order item") - } - - item.ID = itemDB.ID - return nil -} - -func (r *orderRepository) GetListByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int, status string) ([]*entity.Order, error) { - var ordersDB []models.OrderDB - query := r.db.Where("partner_id = ?", partnerID) - - if status != "" { - query = query.Where("status = ?", status) - } - - query = query.Order("created_at DESC") - - if limit > 0 { - query = query.Limit(limit) - } - - if offset > 0 { - query = query.Offset(offset) - } - - if err := query.Find(&ordersDB).Error; err != nil { - return nil, errors.Wrap(err, "failed to find orders by partner ID") - } - - orders := make([]*entity.Order, 0, len(ordersDB)) - for _, orderDB := range ordersDB { - order := r.toDomainOrderModel(&orderDB) - - var orderItems []models.OrderItemDB - if err := r.db.Where("order_id = ?", orderDB.ID).Find(&orderItems).Error; err != nil { - return nil, errors.Wrap(err, "failed to find order items") - } - - order.OrderItems = make([]entity.OrderItem, 0, len(orderItems)) - - for _, itemDB := range orderItems { - item := r.toDomainOrderItemModel(&itemDB) - - orderItem := entity.OrderItem{ - ID: item.ID, - ItemID: item.ItemID, - Quantity: item.Quantity, - ItemName: item.ItemName, - } - - if itemDB.ItemID > 0 { - var product models.ProductDB - err := r.db.First(&product, itemDB.ItemID).Error - - if err == nil { - productDomain := r.toDomainProductModel(&product) - orderItem.Product = productDomain - } - } - - order.OrderItems = append(order.OrderItems, orderItem) - } - - orders = append(orders, order) - } - - return orders, nil -} - -func (r *orderRepository) GetOrderPaymentMethodBreakdown( - ctx mycontext.Context, - partnerID int64, - req entity.SearchRequest, -) ([]entity.PaymentMethodBreakdown, error) { - var breakdown []entity.PaymentMethodBreakdown - - baseQuery := r.db.Model(&models.OrderDB{}).Where("partner_id = ?", partnerID) - - if !req.Start.IsZero() { - baseQuery = baseQuery.Where("created_at >= ?", req.Start) - } - - if !req.End.IsZero() { - baseQuery = baseQuery.Where("created_at <= ?", req.End) - } - - if req.Status != "" { - baseQuery = baseQuery.Where("status = ?", req.Status) - } - - err := baseQuery.Select( - "payment_type, " + - "payment_provider, " + - "COUNT(*) as total_transactions, " + - "SUM(total) as total_amount", - ).Group( - "payment_type, payment_provider", - ).Order("total_amount DESC").Scan(&breakdown).Error - - if err != nil { - return nil, errors.Wrap(err, "failed to get payment method breakdown") - } - - return breakdown, nil -} - -func (r *orderRepository) GetRevenueOverview( - ctx mycontext.Context, - req entity.RevenueOverviewRequest, -) ([]entity.RevenueOverviewItem, error) { - overview := []entity.RevenueOverviewItem{} - - baseQuery := r.db.Model(&models.OrderDB{}). - Where("partner_id = ?", req.PartnerID). - Where("EXTRACT(YEAR FROM created_at) = ?", req.Year) - - if req.Status != "" { - baseQuery = baseQuery.Where("status = ?", req.Status) - } - - switch req.Granularity { - case "m": // Monthly - err := baseQuery.Select( - "TO_CHAR(created_at, 'YYYY-MM') as period, " + - "SUM(total) as total_amount, " + - "COUNT(*) as order_count", - ).Group("period"). - Order("period"). - Scan(&overview).Error - - if err != nil { - return nil, errors.Wrap(err, "failed to get monthly revenue overview") - } - - case "w": // Weekly - err := baseQuery.Select( - "CONCAT(EXTRACT(YEAR FROM created_at), '-W', " + - "LPAD(EXTRACT(WEEK FROM created_at)::text, 2, '0')) as period, " + - "SUM(total) as total_amount, " + - "COUNT(*) as order_count", - ).Group("period"). - Order("period"). - Scan(&overview).Error - - if err != nil { - return nil, errors.Wrap(err, "failed to get weekly revenue overview") - } - - case "d": // Daily - err := baseQuery.Select( - "TO_CHAR(created_at, 'YYYY-MM-DD') as period, " + - "SUM(total) as total_amount, " + - "COUNT(*) as order_count", - ).Group("period"). - Order("period"). - Scan(&overview).Error - - if err != nil { - return nil, errors.Wrap(err, "failed to get daily revenue overview") - } - - default: - return nil, errors.New("invalid granularity. Use 'm' (monthly), 'w' (weekly), or 'd' (daily)") - } - - return overview, nil -} - -func (r *orderRepository) GetSalesByCategory( - ctx mycontext.Context, - req entity.SalesByCategoryRequest, -) ([]entity.SalesByCategoryItem, error) { - salesByCategory := []entity.SalesByCategoryItem{} - - baseQuery := r.db.Model(&models.OrderItemDB{}). - Joins("JOIN orders ON order_items.order_id = orders.id"). - Where("orders.partner_id = ?", req.PartnerID) - - if req.Status != "" { - baseQuery = baseQuery.Where("orders.status = ?", req.Status) - } - - switch req.Period { - case "d": // Daily - baseQuery = baseQuery.Where("DATE(orders.created_at) = CURRENT_DATE") - case "w": // Weekly - baseQuery = baseQuery.Where("DATE_TRUNC('week', orders.created_at) = DATE_TRUNC('week', CURRENT_DATE)") - case "m": // Monthly - baseQuery = baseQuery.Where("DATE_TRUNC('month', orders.created_at) = DATE_TRUNC('month', CURRENT_DATE)") - default: - return nil, errors.New("invalid period. Use 'd' (daily), 'w' (weekly), or 'm' (monthly)") - } - - var totalSales float64 - err := r.db.Model(&models.OrderItemDB{}). - Joins("JOIN orders ON order_items.order_id = orders.id"). - Where("orders.partner_id = ?", req.PartnerID). - Select("COALESCE(SUM(order_items.price * order_items.quantity), 0)"). - Scan(&totalSales).Error - - if err != nil { - return nil, errors.Wrap(err, "failed to calculate total sales") - } - - err = baseQuery.Select( - "order_items.item_type AS category, " + - "COALESCE(SUM(order_items.price * order_items.quantity), 0) AS total_amount, " + - "COALESCE(SUM(order_items.quantity), 0) AS total_quantity", - ). - Group("order_items.item_type"). - Order("total_amount DESC"). - Scan(&salesByCategory).Error - - if err != nil { - return nil, errors.Wrap(err, "failed to get sales by category") - } - - for i := range salesByCategory { - if totalSales > 0 { - salesByCategory[i].Percentage = - (salesByCategory[i].TotalAmount / totalSales) * 100 - } - } - - return salesByCategory, nil -} - -func (r *orderRepository) GetPopularProducts( - ctx mycontext.Context, - req entity.PopularProductsRequest, -) ([]entity.PopularProductItem, error) { - if req.Limit == 0 { - req.Limit = 10 - } - - if req.SortBy != "sales" && req.SortBy != "revenue" { - req.SortBy = "sales" // default to sales - } - - // Base query - baseQuery := r.db.Model(&models.OrderItemDB{}). - Joins("JOIN orders ON order_items.order_id = orders.id"). - Where("orders.partner_id = ?", req.PartnerID) - - if req.Status != "" { - baseQuery = baseQuery.Where("orders.status = ?", req.Status) - } - - switch req.Period { - case "d": // Daily - baseQuery = baseQuery.Where("DATE(orders.created_at) = CURRENT_DATE") - case "w": // Weekly - baseQuery = baseQuery.Where("DATE_TRUNC('week', orders.created_at) = DATE_TRUNC('week', CURRENT_DATE)") - case "m": // Monthly - baseQuery = baseQuery.Where("DATE_TRUNC('month', orders.created_at) = DATE_TRUNC('month', CURRENT_DATE)") - default: - return nil, errors.New("invalid period. Use 'd' (daily), 'w' (weekly), or 'm' (monthly)") - } - - // Calculate total sales/revenue for percentage calculation - var totalSales struct { - TotalAmount float64 - TotalQuantity int64 - } - - err := baseQuery. - Select("COALESCE(SUM(order_items.price * order_items.quantity), 0) as total_amount, " + - "COALESCE(SUM(order_items.quantity), 0) as total_quantity"). - Scan(&totalSales).Error - - if err != nil { - return nil, errors.Wrap(err, "failed to calculate total sales") - } - - popularProducts := []entity.PopularProductItem{} - orderClause := "total_sales DESC" - if req.SortBy == "revenue" { - orderClause = "total_revenue DESC" - } - - err = baseQuery. - Select( - "order_items.item_id AS product_id, " + - "order_items.item_name AS product_name, " + - "order_items.item_type AS category, " + - "COALESCE(SUM(order_items.quantity), 0) AS total_sales, " + - "COALESCE(SUM(order_items.price * order_items.quantity), 0) AS total_revenue, " + - "COALESCE(AVG(order_items.price), 0) AS average_price", - ). - Group("order_items.item_id, order_items.item_name, order_items.item_type"). - Order(orderClause). - Limit(req.Limit). - Scan(&popularProducts).Error - - if err != nil { - return nil, errors.Wrap(err, "failed to get popular products") - } - - for i := range popularProducts { - popularProducts[i].Percentage = - (float64(popularProducts[i].TotalSales) / float64(totalSales.TotalQuantity)) * 100 - } - - return popularProducts, nil -} - -func (r *orderRepository) FindByIDAndPartnerID(ctx mycontext.Context, id int64, partnerID int64) (*entity.Order, error) { - var orderDB models.OrderDB - - if err := r.db.Preload("OrderItems").Where("id = ? AND partner_id = ?", id, partnerID).First(&orderDB).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, errors.New("order not found") - } - return nil, errors.Wrap(err, "failed to find order") - } - - order := r.toDomainOrderModel(&orderDB) - - return order, nil -} - -func (r *orderRepository) FindByIDAndCustomerID(ctx mycontext.Context, id int64, customerID int64) (*entity.Order, error) { - var orderDB models.OrderDB - - if err := r.db.Preload("OrderItems").Where("id = ? AND customer_id = ?", id, customerID).First(&orderDB).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, errors.New("order not found") - } - return nil, errors.Wrap(err, "failed to find order") - } - - order := r.toDomainOrderModel(&orderDB) - - for _, itemDB := range orderDB.OrderItems { - item := r.toDomainOrderItemModel(&itemDB) - order.OrderItems = append(order.OrderItems, *item) - } - - return order, nil -} - -func (r *orderRepository) UpdateOrderItem(ctx mycontext.Context, orderItemID int64, quantity int) error { - now := time.Now() - - result := r.db.Model(&models.OrderItemDB{}). - Where("order_item_id = ?", orderItemID). - Updates(map[string]interface{}{ - "quantity": quantity, - "updated_at": now, - }) - - if result.Error != nil { - return errors.Wrap(result.Error, "failed to update order item") - } - - if result.RowsAffected == 0 { - logger.ContextLogger(ctx).Warn("no order item updated") - } - - return nil -} - -func (r *orderRepository) DeleteOrderItem(ctx mycontext.Context, orderItemID int64) error { - result := r.db.Where("order_item_id = ?", orderItemID).Delete(&models.OrderItemDB{}) - - if result.Error != nil { - return errors.Wrap(result.Error, "failed to delete order item") - } - - if result.RowsAffected == 0 { - logger.ContextLogger(ctx).Warn("no order item deleted") - } - - return nil -} - -func (r *orderRepository) UpdateOrderTotals(ctx mycontext.Context, orderID int64, amount, tax, total float64) error { - now := time.Now() - - result := r.db.Model(&models.OrderDB{}). - Where("id = ?", orderID). - Updates(map[string]interface{}{ - "amount": amount, - "tax": tax, - "total": total, - "updated_at": now, - }) - - if result.Error != nil { - return errors.Wrap(result.Error, "failed to update order totals") - } - - if result.RowsAffected == 0 { - logger.ContextLogger(ctx).Warn("no order updated") - } - - return nil -} - -func (r *orderRepository) UpdateOrderTotalsWithTx(ctx mycontext.Context, trx *gorm.DB, orderID int64, amount, tax, total float64) error { - now := time.Now() - - result := trx.Model(&models.OrderDB{}). - Where("id = ?", orderID). - Updates(map[string]interface{}{ - "amount": amount, - "tax": tax, - "total": total, - "updated_at": now, - }) - - if result.Error != nil { - return errors.Wrap(result.Error, "failed to update order totals") - } - - if result.RowsAffected == 0 { - logger.ContextLogger(ctx).Warn("no order updated") - } - - return nil -} - -func (r *orderRepository) toDomainProductModel(productDB *models.ProductDB) *entity.Product { - if productDB == nil { - return nil - } - - return &entity.Product{ - ID: productDB.ID, - Name: productDB.Name, - Description: productDB.Description, - Price: productDB.Price, - CreatedAt: productDB.CreatedAt, - UpdatedAt: productDB.UpdatedAt, - Type: productDB.Type, - Image: productDB.Image, - } -} diff --git a/internal/repository/order_item_repository.go b/internal/repository/order_item_repository.go new file mode 100644 index 0000000..04ddb59 --- /dev/null +++ b/internal/repository/order_item_repository.go @@ -0,0 +1,136 @@ +package repository + +import ( + "context" + "time" + + "apskel-pos-be/internal/entities" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type OrderItemRepository interface { + Create(ctx context.Context, orderItem *entities.OrderItem) error + GetByID(ctx context.Context, id uuid.UUID) (*entities.OrderItem, error) + GetByOrderID(ctx context.Context, orderID uuid.UUID) ([]*entities.OrderItem, error) + Update(ctx context.Context, orderItem *entities.OrderItem) error + Delete(ctx context.Context, id uuid.UUID) error + RefundOrderItem(ctx context.Context, id uuid.UUID, refundQuantity int, refundAmount float64, reason string, refundedBy uuid.UUID) error + VoidOrderItem(ctx context.Context, id uuid.UUID, voidQuantity int, reason string, voidedBy uuid.UUID) error + UpdateStatus(ctx context.Context, id uuid.UUID, status entities.OrderItemStatus) error +} + +type OrderItemRepositoryImpl struct { + db *gorm.DB +} + +func NewOrderItemRepositoryImpl(db *gorm.DB) *OrderItemRepositoryImpl { + return &OrderItemRepositoryImpl{ + db: db, + } +} + +func (r *OrderItemRepositoryImpl) Create(ctx context.Context, orderItem *entities.OrderItem) error { + return r.db.WithContext(ctx).Create(orderItem).Error +} + +func (r *OrderItemRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID) (*entities.OrderItem, error) { + var orderItem entities.OrderItem + err := r.db.WithContext(ctx).First(&orderItem, "id = ?", id).Error + if err != nil { + return nil, err + } + return &orderItem, nil +} + +func (r *OrderItemRepositoryImpl) GetByOrderID(ctx context.Context, orderID uuid.UUID) ([]*entities.OrderItem, error) { + var orderItems []*entities.OrderItem + err := r.db.WithContext(ctx). + Preload("Product"). + Preload("ProductVariant"). + Where("order_id = ?", orderID). + Find(&orderItems).Error + return orderItems, err +} + +func (r *OrderItemRepositoryImpl) Update(ctx context.Context, orderItem *entities.OrderItem) error { + return r.db.WithContext(ctx).Save(orderItem).Error +} + +func (r *OrderItemRepositoryImpl) Delete(ctx context.Context, id uuid.UUID) error { + return r.db.WithContext(ctx).Delete(&entities.OrderItem{}, "id = ?", id).Error +} + +func (r *OrderItemRepositoryImpl) RefundOrderItem(ctx context.Context, id uuid.UUID, refundQuantity int, refundAmount float64, reason string, refundedBy uuid.UUID) error { + now := time.Now() + + // Get current order item + var orderItem entities.OrderItem + if err := r.db.WithContext(ctx).First(&orderItem, "id = ?", id).Error; err != nil { + return err + } + + // Calculate new refund quantities and amounts + newRefundQuantity := orderItem.RefundQuantity + refundQuantity + newRefundAmount := orderItem.RefundAmount + refundAmount + + // Determine if fully or partially refunded + isFullyRefunded := newRefundQuantity >= orderItem.Quantity + isPartiallyRefunded := newRefundQuantity > 0 && newRefundQuantity < orderItem.Quantity + + updates := map[string]interface{}{ + "refund_quantity": newRefundQuantity, + "refund_amount": newRefundAmount, + "is_partially_refunded": isPartiallyRefunded, + "is_fully_refunded": isFullyRefunded, + "refund_reason": reason, + "refunded_at": now, + "refunded_by": refundedBy, + } + + return r.db.WithContext(ctx).Model(&entities.OrderItem{}). + Where("id = ?", id). + Updates(updates).Error +} + +func (r *OrderItemRepositoryImpl) UpdateStatus(ctx context.Context, id uuid.UUID, status entities.OrderItemStatus) error { + return r.db.WithContext(ctx).Model(&entities.OrderItem{}). + Where("id = ?", id). + Update("status", status).Error +} + +func (r *OrderItemRepositoryImpl) VoidOrderItem(ctx context.Context, id uuid.UUID, voidQuantity int, reason string, voidedBy uuid.UUID) error { + now := time.Now() + + // Get current order item + var orderItem entities.OrderItem + if err := r.db.WithContext(ctx).First(&orderItem, "id = ?", id).Error; err != nil { + return err + } + + // Calculate new voided quantity + newVoidedQuantity := orderItem.RefundQuantity + voidQuantity // Using refund_quantity field for voided quantity + + // Determine if fully or partially voided + isFullyVoided := newVoidedQuantity >= orderItem.Quantity + isPartiallyVoided := newVoidedQuantity > 0 && newVoidedQuantity < orderItem.Quantity + + // Calculate voided amount + voidedAmount := float64(voidQuantity) * orderItem.UnitPrice + + updates := map[string]interface{}{ + "refund_quantity": newVoidedQuantity, // Reusing refund_quantity field for voided quantity + "refund_amount": orderItem.RefundAmount + voidedAmount, + "is_partially_refunded": isPartiallyVoided, // Reusing refunded flags for voided status + "is_fully_refunded": isFullyVoided, + "refund_reason": reason, + "refunded_at": now, + "refunded_by": voidedBy, + "status": entities.OrderItemStatusCancelled, // Mark as cancelled when voided + } + + return r.db.WithContext(ctx).Model(&entities.OrderItem{}). + Where("id = ?", id). + Updates(updates).Error +} diff --git a/internal/repository/order_repository.go b/internal/repository/order_repository.go new file mode 100644 index 0000000..ed5e731 --- /dev/null +++ b/internal/repository/order_repository.go @@ -0,0 +1,237 @@ +package repository + +import ( + "context" + "fmt" + "time" + + "apskel-pos-be/internal/entities" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type OrderRepository interface { + Create(ctx context.Context, order *entities.Order) error + GetByID(ctx context.Context, id uuid.UUID) (*entities.Order, error) + GetWithRelations(ctx context.Context, id uuid.UUID) (*entities.Order, error) + Update(ctx context.Context, order *entities.Order) error + Delete(ctx context.Context, id uuid.UUID) error + List(ctx context.Context, filters map[string]interface{}, limit, offset int) ([]*entities.Order, int64, error) + GetByOrderNumber(ctx context.Context, orderNumber string) (*entities.Order, error) + ExistsByOrderNumber(ctx context.Context, orderNumber string) (bool, error) + VoidOrder(ctx context.Context, id uuid.UUID, reason string, voidedBy uuid.UUID) error + VoidOrderWithStatus(ctx context.Context, id uuid.UUID, status entities.OrderStatus, reason string, voidedBy uuid.UUID) error + RefundOrder(ctx context.Context, id uuid.UUID, reason string, refundedBy uuid.UUID) error + UpdatePaymentStatus(ctx context.Context, id uuid.UUID, status entities.PaymentStatus) error + UpdateStatus(ctx context.Context, id uuid.UUID, status entities.OrderStatus) error + GetNextOrderNumber(ctx context.Context, organizationID, outletID uuid.UUID) (string, error) +} + +type OrderRepositoryImpl struct { + db *gorm.DB +} + +func NewOrderRepositoryImpl(db *gorm.DB) *OrderRepositoryImpl { + return &OrderRepositoryImpl{ + db: db, + } +} + +func (r *OrderRepositoryImpl) Create(ctx context.Context, order *entities.Order) error { + return r.db.WithContext(ctx).Create(order).Error +} + +func (r *OrderRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID) (*entities.Order, error) { + var order entities.Order + err := r.db.WithContext(ctx).First(&order, "id = ?", id).Error + if err != nil { + return nil, err + } + return &order, nil +} + +func (r *OrderRepositoryImpl) GetWithRelations(ctx context.Context, id uuid.UUID) (*entities.Order, error) { + var order entities.Order + err := r.db.WithContext(ctx). + Preload("Organization"). + Preload("Outlet"). + Preload("User"). + Preload("OrderItems"). + Preload("OrderItems.Product"). + Preload("OrderItems.ProductVariant"). + Preload("Payments"). + Preload("Payments.PaymentMethod"). + Preload("Payments.PaymentOrderItems"). + First(&order, "id = ?", id).Error + if err != nil { + return nil, err + } + return &order, nil +} + +func (r *OrderRepositoryImpl) Update(ctx context.Context, order *entities.Order) error { + return r.db.WithContext(ctx).Save(order).Error +} + +func (r *OrderRepositoryImpl) Delete(ctx context.Context, id uuid.UUID) error { + return r.db.WithContext(ctx).Delete(&entities.Order{}, "id = ?", id).Error +} + +func (r *OrderRepositoryImpl) List(ctx context.Context, filters map[string]interface{}, limit, offset int) ([]*entities.Order, int64, error) { + var orders []*entities.Order + var total int64 + + query := r.db.WithContext(ctx).Model(&entities.Order{}). + Preload("Organization"). + Preload("Outlet"). + Preload("User"). + Preload("OrderItems"). + Preload("OrderItems.Product"). + Preload("OrderItems.ProductVariant"). + Preload("Payments"). + Preload("Payments.PaymentMethod") + + for key, value := range filters { + switch key { + case "search": + searchValue := "%" + value.(string) + "%" + query = query.Where("order_number ILIKE ?", searchValue) + case "date_from": + query = query.Where("created_at >= ?", value) + case "date_to": + query = query.Where("created_at <= ?", value) + default: + query = query.Where(key+" = ?", value) + } + } + + if err := query.Count(&total).Error; err != nil { + return nil, 0, err + } + + err := query.Limit(limit).Offset(offset).Order("created_at DESC").Find(&orders).Error + return orders, total, err +} + +func (r *OrderRepositoryImpl) GetByOrderNumber(ctx context.Context, orderNumber string) (*entities.Order, error) { + var order entities.Order + err := r.db.WithContext(ctx).First(&order, "order_number = ?", orderNumber).Error + if err != nil { + return nil, err + } + return &order, nil +} + +func (r *OrderRepositoryImpl) ExistsByOrderNumber(ctx context.Context, orderNumber string) (bool, error) { + var count int64 + err := r.db.WithContext(ctx).Model(&entities.Order{}).Where("order_number = ?", orderNumber).Count(&count).Error + return count > 0, err +} + +func (r *OrderRepositoryImpl) VoidOrder(ctx context.Context, id uuid.UUID, reason string, voidedBy uuid.UUID) error { + now := time.Now() + return r.db.WithContext(ctx).Model(&entities.Order{}). + Where("id = ?", id). + Updates(map[string]interface{}{ + "is_void": true, + "void_reason": reason, + "voided_at": now, + "voided_by": voidedBy, + }).Error +} + +func (r *OrderRepositoryImpl) VoidOrderWithStatus(ctx context.Context, id uuid.UUID, status entities.OrderStatus, reason string, voidedBy uuid.UUID) error { + now := time.Now() + return r.db.WithContext(ctx).Model(&entities.Order{}). + Where("id = ?", id). + Updates(map[string]interface{}{ + "status": status, + "is_void": true, + "void_reason": reason, + "voided_at": now, + "voided_by": voidedBy, + }).Error +} + +func (r *OrderRepositoryImpl) RefundOrder(ctx context.Context, id uuid.UUID, reason string, refundedBy uuid.UUID) error { + now := time.Now() + return r.db.WithContext(ctx).Model(&entities.Order{}). + Where("id = ?", id). + Updates(map[string]interface{}{ + "is_refund": true, + "refund_reason": reason, + "refunded_at": now, + "refunded_by": refundedBy, + }).Error +} + +func (r *OrderRepositoryImpl) UpdatePaymentStatus(ctx context.Context, id uuid.UUID, status entities.PaymentStatus) error { + return r.db.WithContext(ctx).Model(&entities.Order{}). + Where("id = ?", id). + Update("payment_status", status).Error +} + +func (r *OrderRepositoryImpl) UpdateStatus(ctx context.Context, id uuid.UUID, status entities.OrderStatus) error { + return r.db.WithContext(ctx).Model(&entities.Order{}). + Where("id = ?", id). + Update("status", status).Error +} + +func (r *OrderRepositoryImpl) GetNextOrderNumber(ctx context.Context, organizationID, outletID uuid.UUID) (string, error) { + now := time.Now() + year := now.Year() + month := int(now.Month()) + + // Use a transaction to ensure atomic sequence increment + tx := r.db.WithContext(ctx).Begin() + if tx.Error != nil { + return "", tx.Error + } + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() + + // Get or create sequence record + var sequence entities.OrderSequence + err := tx.Where("organization_id = ? AND outlet_id = ? AND year = ? AND month = ?", + organizationID, outletID, year, month). + First(&sequence).Error + + if err != nil { + if err == gorm.ErrRecordNotFound { + // Create new sequence record + sequence = entities.OrderSequence{ + OrganizationID: organizationID, + OutletID: outletID, + Year: year, + Month: month, + SequenceNumber: 0, + } + if err := tx.Create(&sequence).Error; err != nil { + tx.Rollback() + return "", err + } + } else { + tx.Rollback() + return "", err + } + } + + // Increment sequence number + sequence.SequenceNumber++ + if err := tx.Save(&sequence).Error; err != nil { + tx.Rollback() + return "", err + } + + // Commit transaction + if err := tx.Commit().Error; err != nil { + return "", err + } + + orderNumber := fmt.Sprintf("ORD/%04d%02d/%06d", year, month, sequence.SequenceNumber) + return orderNumber, nil +} diff --git a/internal/repository/orders/order.go b/internal/repository/orders/order.go deleted file mode 100644 index 6d20982..0000000 --- a/internal/repository/orders/order.go +++ /dev/null @@ -1,354 +0,0 @@ -package orders - -import ( - "context" - "enaklo-pos-be/internal/common/logger" - "enaklo-pos-be/internal/common/mycontext" - "enaklo-pos-be/internal/entity" - "strings" - "time" - - "go.uber.org/zap" - "gorm.io/gorm" -) - -type OrderRepository struct { - db *gorm.DB -} - -func NewOrderRepository(db *gorm.DB) *OrderRepository { - return &OrderRepository{ - db: db, - } -} - -func (r *OrderRepository) Create(ctx context.Context, order *entity.Order) (*entity.Order, error) { - err := r.db.WithContext(ctx).Create(order).Error - if err != nil { - logger.ContextLogger(ctx).Error("error when creating order", zap.Error(err)) - return nil, err - } - return r.FindByID(ctx, order.ID) -} - -func (r *OrderRepository) UpdateStatus(ctx context.Context, orderID int64, status string) (*entity.Order, error) { - order := new(entity.Order) - if err := r.db.WithContext(ctx).First(order, orderID).Error; err != nil { - logger.ContextLogger(ctx).Error("error when finding order", zap.Error(err)) - return nil, err - } - order.Status = status - if err := r.db.WithContext(ctx).Save(order).Error; err != nil { - logger.ContextLogger(ctx).Error("error when updating order status", zap.Error(err)) - return nil, err - } - return order, nil -} - -func (r *OrderRepository) FindByID(ctx context.Context, id int64) (*entity.Order, error) { - var order entity.Order - - err := r.db.WithContext(ctx). - Preload("OrderItems", func(db *gorm.DB) *gorm.DB { - return db.Preload("Product") - }). - Preload("Site"). - Preload("User"). - Preload("Payment"). - First(&order, id).Error - - if err != nil { - logger.ContextLogger(ctx).Error("error when finding order by ID", zap.Error(err)) - return nil, err - } - - return &order, nil -} - -func (r *OrderRepository) FindPrintDetailByID(ctx context.Context, id int64) (*entity.OrderPrintDetail, error) { - var printDetail entity.OrderPrintDetail - - err := r.db.WithContext(ctx). - Table("orders"). - Select("orders.id, partners.name as partner_name, sites.name as site_name, partners.logo as logo, orders.ref_id as order_id, "+ - "orders.visit_date, orders.payment_type, orders.source, "+ - "orders.ticket_status, orders.total, orders.fee"). - Joins("JOIN partners ON partners.id = orders.partner_id"). - Joins("JOIN sites ON sites.id = orders.site_id"). - Where("orders.id = ?", id). - Scan(&printDetail).Error - - if err == nil { - err = r.db.WithContext(ctx).Where("order_id = ?", id).Preload("Product").Find(&printDetail.OrderItems).Error - } - - if err != nil { - logger.ContextLogger(ctx).Error("error when finding print detail by ID", zap.Error(err)) - return nil, err - } - - return &printDetail, nil -} - -func (r *OrderRepository) FindByQRCode(ctx context.Context, refID string) (*entity.Order, error) { - var order entity.Order - - err := r.db.WithContext(ctx). - Preload("OrderItems", func(db *gorm.DB) *gorm.DB { - return db.Preload("Product") - }). - Preload("User"). - Preload("Payment"). - Where("ref_id = ?", refID). - First(&order).Error - - if err != nil { - logger.ContextLogger(ctx).Error("error when finding order by refID", zap.Error(err)) - return nil, err - } - - return &order, nil -} - -func (r *OrderRepository) SetOrderStatus(ctx context.Context, db *gorm.DB, orderID int64, status string) error { - var order entity.Order - if err := db.WithContext(ctx).Preload("OrderItems").First(&order, orderID).Error; err != nil { - logger.ContextLogger(ctx).Error("error when finding order by ID", zap.Error(err)) - return err - } - - order.Status = status - - if err := db.WithContext(ctx).Save(&order).Error; err != nil { - logger.ContextLogger(ctx).Error("error when updating order status", zap.Error(err)) - return err - } - - return nil -} - -func (r *OrderRepository) Update(ctx context.Context, order *entity.Order) (*entity.Order, error) { - if err := r.db.WithContext(ctx).Model(&entity.Order{}).Where("id = ?", order.ID).Updates(order).Error; err != nil { - logger.ContextLogger(ctx).Error("error when updating order", zap.Error(err)) - return nil, err - } - return order, nil -} - -func (b *OrderRepository) GetAllHystoryOrders(ctx context.Context, req entity.OrderSearch) (entity.HistoryOrderList, int, error) { - var orders []*entity.HistoryOrderDB - var total int64 - - query := b.db.Table("orders"). - Select("orders.id as id, users.name as employee, sites.name as site, orders.created_at as timestamp, orders.created_at as booking_time, STRING_AGG(ticket_summary.name || ' x' || ticket_summary.total_qty, ', ') AS tickets, orders.payment_type as payment_type, orders.status as status, orders.amount as amount, orders.visit_date as visit_date, orders.ticket_status as ticket_status, orders.source as source"). - Joins("left join (SELECT items.order_id, products.name, SUM(items.quantity) AS total_qty FROM order_items items LEFT JOIN products ON items.item_id = products.id GROUP BY items.order_id, products.name) AS ticket_summary ON orders.id = ticket_summary.order_id"). - Joins("left join users on orders.created_by = users.id"). - Where("orders.payment_type != ?", "NEW") - - if req.PaymentType != "" { - query = query.Where("orders.payment_type = ?", req.PaymentType) - } - - if req.CreatedBy != 0 { - query = query.Where("orders.created_by = ?", req.CreatedBy) - } - - if req.Status != "" { - query = query.Where("orders.status = ?", req.Status) - } - - if !req.IsAdmin && !req.IsCustomer { - query = query.Where("orders.partner_id = ?", req.PartnerID) - } - - if req.StartDate != "" && req.EndDate != "" { - // Assuming req.Date and req.EndDate are in string format "YYYY-MM-DD" - startDate := req.StartDate + " 00:00:00" - endDate := req.EndDate + " 23:59:59" - - query = query.Where("orders.created_at BETWEEN ? AND ?", startDate, endDate) - } else { - currentTime := time.Now() - startOfMonth := time.Date(currentTime.Year(), currentTime.Month(), 1, 0, 0, 0, 0, time.Local) - endOfMonth := startOfMonth.AddDate(0, 1, 0).Add(-time.Second) - query = query.Where("orders.created_at BETWEEN ? AND ?", startOfMonth, endOfMonth) - } - - if req.SiteID != nil { - query = query.Where("orders.partner_id = ?", req.SiteID) - } - - if req.Source != "" { - query = query.Where("orders.source = ?", req.Source) - } - - query = query.Group("orders.id, users.name, sites.name, orders.created_at, orders.payment_type, orders.status") - - query = query.Order("orders.created_at DESC") - - if err := query.Count(&total).Error; err != nil { - logger.ContextLogger(ctx).Error("error when count history orders", zap.Error(err)) - return nil, 0, err - } - - if req.Offset > 0 { - query = query.Offset(req.Offset) - } - - if req.Limit > 0 { - query = query.Limit(req.Limit) - } - - if err := query.Debug().Scan(&orders).Error; err != nil { - logger.ContextLogger(ctx).Error("error when get all history orders", zap.Error(err)) - return nil, 0, err - } - - for i, order := range orders { - if order.RawTickets != "" { - orders[i].Tickets = strings.Split(order.RawTickets, ", ") - } - } - - return orders, int(total), nil -} - -func (r *OrderRepository) CountSoldOfTicket(ctx mycontext.Context, req entity.OrderSearch) (*entity.TicketSoldDB, error) { - ticketCount := new(entity.TicketSoldDB) - - query := r.db.Table("orders"). - Select("sum(items.qty) as count"). - Joins("left join order_items items on orders.id = items.order_id"). - Where("orders.status = ?", "PAID"). - Where("EXTRACT(MONTH FROM orders.created_at) = ?", time.Now().Month()) - - if !req.IsAdmin { - query = query.Where("orders.partner_id = ?", req.PartnerID) - } - - if err := query.Scan(&ticketCount).Error; err != nil { - logger.ContextLogger(ctx).Error("error when get count ticket", zap.Error(err)) - return nil, err - } - - return ticketCount, nil -} - -func (r *OrderRepository) SumAmount(ctx mycontext.Context, req entity.OrderSearch) (*entity.OrderDB, error) { - amount := new(entity.OrderDB) - - query := r.db.Table("orders"). - Select("sum(amount) as amount"). - Where("status = ?", "PAID"). - Where("EXTRACT(MONTH FROM orders.created_at) = ?", time.Now().Month()) - - if req.PaymentType == "CASH" { - query = query.Where("payment_type = ?", req.PaymentType) - } else { - query = query.Where("payment_type != ?", "CASH") - } - - if req.PartnerID != nil { - query = query.Where("orders.partner_id = ?", req.PartnerID) - } - - if err := query.Scan(&amount).Error; err != nil { - logger.ContextLogger(ctx).Error("error when get cash amount", zap.Error(err)) - return nil, err - } - - return amount, nil -} - -func (r *OrderRepository) GetDailySalesMetrics(ctx context.Context, req entity.OrderSearch) ([]entity.ProductDailySales, error) { - var sales []entity.ProductDailySales - - var dateTrunc, periodFilter string - now := time.Now() - - switch req.Period { - case "1d": - dateTrunc = "hour" - periodFilter = now.Add(-24 * time.Hour).Format("2006-01-02 15:04:05") - case "7d": - dateTrunc = "day" - periodFilter = now.Add(-7 * 24 * time.Hour).Format("2006-01-02 15:04:05") - case "1m": - dateTrunc = "day" - periodFilter = now.AddDate(0, -1, 0).Format("2006-01-02 15:04:05") - case "1y": - dateTrunc = "week" - periodFilter = now.AddDate(-1, 0, 0).Format("2006-01-02 15:04:05") - default: - dateTrunc = "day" - periodFilter = now.AddDate(0, -1, 0).Format("2006-01-02 15:04:05") // Default to last month - } - - // Build the query with GORM - query := r.db.WithContext(ctx). - Table("orders o"). - Select(`DATE_TRUNC(?, o.created_at) AS day, s.id AS site_id, s.name AS site_name, o.payment_type, SUM(oi.qty * oi.price) AS total`, dateTrunc). - Joins("JOIN order_items oi ON o.id = oi.order_id"). - Joins("JOIN sites s ON o.site_id = s.id"). - Where("o.status = ?", "PAID"). - Where("o.created_at >= ?", periodFilter) - - if req.PartnerID != nil { - query = query.Where("o.partner_id = ?", *req.PartnerID) - } - - if req.SiteID != nil { - query = query.Where("o.site_id = ?", *req.SiteID) - } - - query = query.Group("day, s.id, s.name, o.payment_type"). - Order("day") - - if err := query.Find(&sales).Error; err != nil { - return nil, err - } - - return sales, nil -} - -func (r *OrderRepository) GetPaymentTypeDistribution(ctx context.Context, req entity.OrderSearch) ([]entity.PaymentTypeDistribution, error) { - var distribution []entity.PaymentTypeDistribution - - var periodFilter string - now := time.Now() - - switch req.Period { - case "1d": - periodFilter = now.Add(-24 * time.Hour).Format("2006-01-02 15:04:05") - case "7d": - periodFilter = now.Add(-7 * 24 * time.Hour).Format("2006-01-02 15:04:05") - case "1m": - periodFilter = now.AddDate(0, -1, 0).Format("2006-01-02 15:04:05") - case "1y": - periodFilter = now.AddDate(-1, 0, 0).Format("2006-01-02 15:04:05") - default: - periodFilter = now.AddDate(0, -1, 0).Format("2006-01-02 15:04:05") // Default to last month - } - - query := r.db.WithContext(ctx). - Table("orders o"). - Select("payment_type, COUNT(*) as count"). - Where("status = ?", "PAID"). - Where("o.created_at >= ?", periodFilter) - - if req.PartnerID != nil { - query = query.Where("o.partner_id = ?", *req.PartnerID) - } - - if req.SiteID != nil { - query = query.Where("o.site_id = ?", *req.SiteID) - } - - query = query.Group("payment_type") - - if err := query.Scan(&distribution).Error; err != nil { - return nil, err - } - - return distribution, nil -} diff --git a/internal/repository/organization_repository.go b/internal/repository/organization_repository.go new file mode 100644 index 0000000..8cbf1b7 --- /dev/null +++ b/internal/repository/organization_repository.go @@ -0,0 +1,101 @@ +package repository + +import ( + "context" + "github.com/google/uuid" + + "apskel-pos-be/internal/entities" + + "gorm.io/gorm" +) + +type OrganizationRepositoryImpl struct { + db *gorm.DB +} + +func NewOrganizationRepositoryImpl(db *gorm.DB) *OrganizationRepositoryImpl { + return &OrganizationRepositoryImpl{ + db: db, + } +} + +func (r *OrganizationRepositoryImpl) Create(ctx context.Context, org *entities.Organization) error { + return r.db.WithContext(ctx).Create(org).Error +} + +func (r *OrganizationRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID) (*entities.Organization, error) { + var org entities.Organization + err := r.db.WithContext(ctx).First(&org, "id = ?", id).Error + if err != nil { + return nil, err + } + return &org, nil +} + +func (r *OrganizationRepositoryImpl) GetWithOutlets(ctx context.Context, id uuid.UUID) (*entities.Organization, error) { + var org entities.Organization + err := r.db.WithContext(ctx).Preload("Outlets").First(&org, "id = ?", id).Error + if err != nil { + return nil, err + } + return &org, nil +} + +func (r *OrganizationRepositoryImpl) GetByPlanType(ctx context.Context, planType string) ([]*entities.Organization, error) { + var organizations []*entities.Organization + err := r.db.WithContext(ctx).Where("plan_type = ?", planType).Find(&organizations).Error + return organizations, err +} + +func (r *OrganizationRepositoryImpl) UpdatePlanType(ctx context.Context, id uuid.UUID, planType string) error { + return r.db.WithContext(ctx).Model(&entities.Organization{}). + Where("id = ?", id). + Update("plan_type", planType).Error +} + +func (r *OrganizationRepositoryImpl) Update(ctx context.Context, org *entities.Organization) error { + return r.db.WithContext(ctx).Save(org).Error +} + +func (r *OrganizationRepositoryImpl) Delete(ctx context.Context, id uuid.UUID) error { + return r.db.WithContext(ctx).Delete(&entities.Organization{}, "id = ?", id).Error +} + +func (r *OrganizationRepositoryImpl) List(ctx context.Context, filters map[string]interface{}, limit, offset int) ([]*entities.Organization, int64, error) { + var organizations []*entities.Organization + var total int64 + + query := r.db.WithContext(ctx).Model(&entities.Organization{}) + + for key, value := range filters { + query = query.Where(key+" = ?", value) + } + + if err := query.Count(&total).Error; err != nil { + return nil, 0, err + } + + err := query.Limit(limit).Offset(offset).Find(&organizations).Error + return organizations, total, err +} + +func (r *OrganizationRepositoryImpl) Count(ctx context.Context, filters map[string]interface{}) (int64, error) { + var count int64 + query := r.db.WithContext(ctx).Model(&entities.Organization{}) + + for key, value := range filters { + query = query.Where(key+" = ?", value) + } + + err := query.Count(&count).Error + return count, err +} + +func (r *OrganizationRepositoryImpl) GetByEmail(ctx context.Context, email string) (*entities.Organization, error) { + var org entities.Organization + err := r.db.WithContext(ctx).First(&org, "email = ?", email).Error + if err != nil { + return nil, err + } + return &org, nil +} diff --git a/internal/repository/outlet_repository.go b/internal/repository/outlet_repository.go new file mode 100644 index 0000000..4e46c81 --- /dev/null +++ b/internal/repository/outlet_repository.go @@ -0,0 +1,105 @@ +package repository + +import ( + "apskel-pos-be/internal/entities" + "context" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type OutletRepositoryImpl struct { + db *gorm.DB +} + +func NewOutletRepositoryImpl(db *gorm.DB) *OutletRepositoryImpl { + return &OutletRepositoryImpl{ + db: db, + } +} + +func (r *OutletRepositoryImpl) Create(ctx context.Context, outlet *entities.Outlet) error { + return r.db.WithContext(ctx).Create(outlet).Error +} + +func (r *OutletRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID) (*entities.Outlet, error) { + var outlet entities.Outlet + err := r.db.WithContext(ctx).First(&outlet, "id = ?", id).Error + if err != nil { + return nil, err + } + return &outlet, nil +} + +func (r *OutletRepositoryImpl) GetWithOrders(ctx context.Context, id uuid.UUID) (*entities.Outlet, error) { + var outlet entities.Outlet + err := r.db.WithContext(ctx).Preload("Orders").First(&outlet, "id = ?", id).Error + if err != nil { + return nil, err + } + return &outlet, nil +} + +func (r *OutletRepositoryImpl) GetByOrganizationID(ctx context.Context, organizationID uuid.UUID) ([]*entities.Outlet, error) { + var outlets []*entities.Outlet + err := r.db.WithContext(ctx).Where("organization_id = ?", organizationID).Find(&outlets).Error + return outlets, err +} + +func (r *OutletRepositoryImpl) GetByOrganizationIDWithPagination(ctx context.Context, organizationID uuid.UUID, limit, offset int) ([]*entities.Outlet, int64, error) { + var outlets []*entities.Outlet + var total int64 + + query := r.db.WithContext(ctx).Where("organization_id = ?", organizationID) + + if err := query.Model(&entities.Outlet{}).Count(&total).Error; err != nil { + return nil, 0, err + } + + err := query.Limit(limit).Offset(offset).Find(&outlets).Error + return outlets, total, err +} + +func (r *OutletRepositoryImpl) Update(ctx context.Context, outlet *entities.Outlet) error { + return r.db.WithContext(ctx).Save(outlet).Error +} + +func (r *OutletRepositoryImpl) Delete(ctx context.Context, id uuid.UUID) error { + return r.db.WithContext(ctx).Delete(&entities.Outlet{}, "id = ?", id).Error +} + +func (r *OutletRepositoryImpl) UpdateActiveStatus(ctx context.Context, id uuid.UUID, isActive bool) error { + return r.db.WithContext(ctx).Model(&entities.Outlet{}). + Where("id = ?", id). + Update("is_active", isActive).Error +} + +func (r *OutletRepositoryImpl) List(ctx context.Context, filters map[string]interface{}, limit, offset int) ([]*entities.Outlet, int64, error) { + var outlets []*entities.Outlet + var total int64 + + query := r.db.WithContext(ctx).Model(&entities.Outlet{}) + + for key, value := range filters { + query = query.Where(key+" = ?", value) + } + + if err := query.Count(&total).Error; err != nil { + return nil, 0, err + } + + err := query.Limit(limit).Offset(offset).Find(&outlets).Error + return outlets, total, err +} + +func (r *OutletRepositoryImpl) Count(ctx context.Context, filters map[string]interface{}) (int64, error) { + var count int64 + query := r.db.WithContext(ctx).Model(&entities.Outlet{}) + + for key, value := range filters { + query = query.Where(key+" = ?", value) + } + + err := query.Count(&count).Error + return count, err +} diff --git a/internal/repository/outlet_setting_repository.go b/internal/repository/outlet_setting_repository.go new file mode 100644 index 0000000..9933f3f --- /dev/null +++ b/internal/repository/outlet_setting_repository.go @@ -0,0 +1,87 @@ +package repository + +import ( + "apskel-pos-be/internal/entities" + "context" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type OutletSettingRepositoryImpl struct { + db *gorm.DB +} + +func NewOutletSettingRepositoryImpl(db *gorm.DB) *OutletSettingRepositoryImpl { + return &OutletSettingRepositoryImpl{ + db: db, + } +} + +func (r *OutletSettingRepositoryImpl) Create(ctx context.Context, setting *entities.OutletSetting) error { + return r.db.WithContext(ctx).Create(setting).Error +} + +func (r *OutletSettingRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID) (*entities.OutletSetting, error) { + var setting entities.OutletSetting + err := r.db.WithContext(ctx).First(&setting, "id = ?", id).Error + if err != nil { + return nil, err + } + return &setting, nil +} + +func (r *OutletSettingRepositoryImpl) GetByOutletIDAndKey(ctx context.Context, outletID uuid.UUID, key string) (*entities.OutletSetting, error) { + var setting entities.OutletSetting + err := r.db.WithContext(ctx).Where("outlet_id = ? AND key = ?", outletID, key).First(&setting).Error + if err != nil { + return nil, err + } + return &setting, nil +} + +func (r *OutletSettingRepositoryImpl) GetByOutletID(ctx context.Context, outletID uuid.UUID) ([]*entities.OutletSetting, error) { + var settings []*entities.OutletSetting + err := r.db.WithContext(ctx).Where("outlet_id = ?", outletID).Find(&settings).Error + return settings, err +} + +func (r *OutletSettingRepositoryImpl) Update(ctx context.Context, setting *entities.OutletSetting) error { + return r.db.WithContext(ctx).Save(setting).Error +} + +func (r *OutletSettingRepositoryImpl) Upsert(ctx context.Context, setting *entities.OutletSetting) error { + return r.db.WithContext(ctx).Save(setting).Error +} + +func (r *OutletSettingRepositoryImpl) Delete(ctx context.Context, id uuid.UUID) error { + return r.db.WithContext(ctx).Delete(&entities.OutletSetting{}, "id = ?", id).Error +} + +func (r *OutletSettingRepositoryImpl) DeleteByOutletIDAndKey(ctx context.Context, outletID uuid.UUID, key string) error { + return r.db.WithContext(ctx).Delete(&entities.OutletSetting{}, "outlet_id = ? AND key = ?", outletID, key).Error +} + +func (r *OutletSettingRepositoryImpl) GetPrinterSettingsByOutletID(ctx context.Context, outletID uuid.UUID) (map[string]string, error) { + var settings []*entities.OutletSetting + err := r.db.WithContext(ctx).Where("outlet_id = ? AND key IN (?, ?, ?, ?, ?, ?)", + outletID, + "printer_outlet_name", + "printer_address", + "printer_phone_number", + "printer_paper_size", + "printer_footer", + "printer_footer_hashtag", + ).Find(&settings).Error + + if err != nil { + return nil, err + } + + result := make(map[string]string) + for _, setting := range settings { + result[setting.Key] = setting.Value + } + + return result, nil +} diff --git a/internal/repository/partner_settings.go b/internal/repository/partner_settings.go deleted file mode 100644 index 660d67a..0000000 --- a/internal/repository/partner_settings.go +++ /dev/null @@ -1,225 +0,0 @@ -package repository - -import ( - "enaklo-pos-be/internal/common/mycontext" - "enaklo-pos-be/internal/entity" - "enaklo-pos-be/internal/repository/models" - "github.com/pkg/errors" - "gorm.io/gorm" - "time" -) - -type PartnerSettingsRepository interface { - GetByPartnerID(ctx mycontext.Context, partnerID int64) (*entity.PartnerSettings, error) - Upsert(ctx mycontext.Context, settings *entity.PartnerSettings) error - GetPaymentMethods(ctx mycontext.Context, partnerID int64) ([]entity.PartnerPaymentMethod, error) - UpsertPaymentMethod(ctx mycontext.Context, method *entity.PartnerPaymentMethod) error - DeletePaymentMethod(ctx mycontext.Context, id int64, partnerID int64) error - UpdatePaymentMethodOrder(ctx mycontext.Context, partnerID int64, methodIDs []int64) error -} - -type partnerSettingsRepository struct { - db *gorm.DB -} - -func NewPartnerSettingsRepository(db *gorm.DB) PartnerSettingsRepository { - return &partnerSettingsRepository{db: db} -} - -func (r *partnerSettingsRepository) GetByPartnerID(ctx mycontext.Context, partnerID int64) (*entity.PartnerSettings, error) { - var settingsDB models.PartnerSettingsDB - - err := r.db.Where("partner_id = ?", partnerID).First(&settingsDB).Error - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return &entity.PartnerSettings{ - PartnerID: partnerID, - TaxEnabled: true, - TaxPercentage: 10.0, - }, nil - } - return nil, errors.Wrap(err, "failed to get partner settings") - } - - return r.toDomainModel(&settingsDB), nil -} - -func (r *partnerSettingsRepository) Upsert(ctx mycontext.Context, settings *entity.PartnerSettings) error { - settingsDB := r.toDBModel(settings) - settingsDB.UpdatedAt = time.Now() - - // Check if record exists - var count int64 - if err := r.db.Model(&models.PartnerSettingsDB{}).Where("partner_id = ?", settings.PartnerID).Count(&count).Error; err != nil { - return errors.Wrap(err, "failed to check partner settings existence") - } - - if count > 0 { - // Update existing record - if err := r.db.Model(&models.PartnerSettingsDB{}).Where("partner_id = ?", settings.PartnerID).Updates(settingsDB).Error; err != nil { - return errors.Wrap(err, "failed to update partner settings") - } - } else { - // Create new record - settingsDB.CreatedAt = time.Now() - if err := r.db.Create(&settingsDB).Error; err != nil { - return errors.Wrap(err, "failed to create partner settings") - } - } - - return nil -} - -func (r *partnerSettingsRepository) GetPaymentMethods(ctx mycontext.Context, partnerID int64) ([]entity.PartnerPaymentMethod, error) { - var methodsDB []models.PartnerPaymentMethodDB - - if err := r.db.Where("partner_id = ?", partnerID).Order("display_order").Find(&methodsDB).Error; err != nil { - return nil, errors.Wrap(err, "failed to get partner payment methods") - } - - methods := make([]entity.PartnerPaymentMethod, len(methodsDB)) - for i, methodDB := range methodsDB { - methods[i] = *r.toDomainPaymentMethodModel(&methodDB) - } - - return methods, nil -} - -func (r *partnerSettingsRepository) UpsertPaymentMethod(ctx mycontext.Context, method *entity.PartnerPaymentMethod) error { - methodDB := r.toDBPaymentMethodModel(method) - methodDB.UpdatedAt = time.Now() - - tx := r.db.Begin() - if tx.Error != nil { - return errors.Wrap(tx.Error, "failed to begin transaction") - } - - defer func() { - if r := recover(); r != nil { - tx.Rollback() - } - }() - - if method.ID > 0 { - // Update existing record - if err := tx.Model(&models.PartnerPaymentMethodDB{}).Where("id = ? AND partner_id = ?", method.ID, method.PartnerID).Updates(methodDB).Error; err != nil { - tx.Rollback() - return errors.Wrap(err, "failed to update payment method") - } - } else { - // Get the next display order if not specified - if method.DisplayOrder == 0 { - var maxOrder int - if err := tx.Model(&models.PartnerPaymentMethodDB{}).Where("partner_id = ?", method.PartnerID).Select("COALESCE(MAX(display_order), 0)").Row().Scan(&maxOrder); err != nil { - tx.Rollback() - return errors.Wrap(err, "failed to get max display order") - } - methodDB.DisplayOrder = maxOrder + 1 - } - - // Create new record - methodDB.CreatedAt = time.Now() - if err := tx.Create(&methodDB).Error; err != nil { - tx.Rollback() - return errors.Wrap(err, "failed to create payment method") - } - - method.ID = methodDB.ID - } - - return tx.Commit().Error -} - -func (r *partnerSettingsRepository) DeletePaymentMethod(ctx mycontext.Context, id int64, partnerID int64) error { - result := r.db.Where("id = ? AND partner_id = ?", id, partnerID).Delete(&models.PartnerPaymentMethodDB{}) - - if result.Error != nil { - return errors.Wrap(result.Error, "failed to delete payment method") - } - - if result.RowsAffected == 0 { - return errors.New("payment method not found or not authorized") - } - - return nil -} - -func (r *partnerSettingsRepository) UpdatePaymentMethodOrder(ctx mycontext.Context, partnerID int64, methodIDs []int64) error { - tx := r.db.Begin() - if tx.Error != nil { - return errors.Wrap(tx.Error, "failed to begin transaction") - } - - defer func() { - if r := recover(); r != nil { - tx.Rollback() - } - }() - - for i, id := range methodIDs { - if err := tx.Model(&models.PartnerPaymentMethodDB{}). - Where("id = ? AND partner_id = ?", id, partnerID). - Update("display_order", i+1).Error; err != nil { - tx.Rollback() - return errors.Wrap(err, "failed to update payment method order") - } - } - - return tx.Commit().Error -} - -func (r *partnerSettingsRepository) toDomainModel(dbModel *models.PartnerSettingsDB) *entity.PartnerSettings { - return &entity.PartnerSettings{ - PartnerID: dbModel.PartnerID, - TaxEnabled: dbModel.TaxEnabled, - TaxPercentage: dbModel.TaxPercentage, - InvoicePrefix: dbModel.InvoicePrefix, - BusinessHours: dbModel.BusinessHours, - LogoURL: dbModel.LogoURL, - ThemeColor: dbModel.ThemeColor, - ReceiptFooterText: dbModel.ReceiptFooterText, - ReceiptHeaderText: dbModel.ReceiptHeaderText, - CreatedAt: dbModel.CreatedAt, - UpdatedAt: dbModel.UpdatedAt, - } -} - -func (r *partnerSettingsRepository) toDBModel(domainModel *entity.PartnerSettings) models.PartnerSettingsDB { - return models.PartnerSettingsDB{ - PartnerID: domainModel.PartnerID, - TaxEnabled: domainModel.TaxEnabled, - TaxPercentage: domainModel.TaxPercentage, - InvoicePrefix: domainModel.InvoicePrefix, - BusinessHours: domainModel.BusinessHours, - LogoURL: domainModel.LogoURL, - ThemeColor: domainModel.ThemeColor, - ReceiptFooterText: domainModel.ReceiptFooterText, - ReceiptHeaderText: domainModel.ReceiptHeaderText, - } -} - -func (r *partnerSettingsRepository) toDomainPaymentMethodModel(dbModel *models.PartnerPaymentMethodDB) *entity.PartnerPaymentMethod { - return &entity.PartnerPaymentMethod{ - ID: dbModel.ID, - PartnerID: dbModel.PartnerID, - PaymentMethod: dbModel.PaymentMethod, - IsEnabled: dbModel.IsEnabled, - DisplayName: dbModel.DisplayName, - DisplayOrder: dbModel.DisplayOrder, - AdditionalInfo: dbModel.AdditionalInfo, - CreatedAt: dbModel.CreatedAt, - UpdatedAt: dbModel.UpdatedAt, - } -} - -func (r *partnerSettingsRepository) toDBPaymentMethodModel(domainModel *entity.PartnerPaymentMethod) models.PartnerPaymentMethodDB { - return models.PartnerPaymentMethodDB{ - ID: domainModel.ID, - PartnerID: domainModel.PartnerID, - PaymentMethod: domainModel.PaymentMethod, - IsEnabled: domainModel.IsEnabled, - DisplayName: domainModel.DisplayName, - DisplayOrder: domainModel.DisplayOrder, - AdditionalInfo: domainModel.AdditionalInfo, - } -} diff --git a/internal/repository/partners/partners.go b/internal/repository/partners/partners.go deleted file mode 100644 index cb5c10c..0000000 --- a/internal/repository/partners/partners.go +++ /dev/null @@ -1,143 +0,0 @@ -package partners - -import ( - "context" - "enaklo-pos-be/internal/common/logger" - "enaklo-pos-be/internal/entity" - - "go.uber.org/zap" - "gorm.io/gorm" -) - -type PartnerRepository struct { - db *gorm.DB -} - -func NewPartnerRepository(db *gorm.DB) *PartnerRepository { - return &PartnerRepository{ - db: db, - } -} - -func (b *PartnerRepository) Create(ctx context.Context, Partner *entity.PartnerDB) (*entity.PartnerDB, error) { - err := b.db.Create(Partner).Error - if err != nil { - logger.ContextLogger(ctx).Error("error when create Partner", zap.Error(err)) - return nil, err - } - return Partner, nil -} - -func (b *PartnerRepository) CreateWithTx(ctx context.Context, tx *gorm.DB, Partner *entity.PartnerDB) (*entity.PartnerDB, error) { - err := tx.Create(Partner).Error - if err != nil { - logger.ContextLogger(ctx).Error("error when create Partner", zap.Error(err)) - return nil, err - } - return Partner, nil -} - -func (b *PartnerRepository) Update(ctx context.Context, Partner *entity.PartnerDB) (*entity.PartnerDB, error) { - if err := b.db.Save(Partner).Error; err != nil { - logger.ContextLogger(ctx).Error("error when update Partner", zap.Error(err)) - return nil, err - } - return Partner, nil -} - -func (b *PartnerRepository) UpdateWithTx(ctx context.Context, tx *gorm.DB, Partner *entity.PartnerDB) (*entity.PartnerDB, error) { - if err := tx.Save(Partner).Error; err != nil { - logger.ContextLogger(ctx).Error("error when update Partner", zap.Error(err)) - return nil, err - } - return Partner, nil -} - -func (b *PartnerRepository) GetByID(ctx context.Context, id int64) (*entity.PartnerDB, error) { - Partner := new(entity.PartnerDBSearch) - - query := b.db.Table("partners p"). - Select("p.*, w.balance as wallet_balance, u.name as admin_name, u.email as admin_email, u.phone_number as admin_phone_number"). - Joins("LEFT JOIN wallets w ON w.partner_id = p.id"). - Joins("LEFT JOIN users u ON p.admin_user_id = u.id"). - Where("p.id = ? AND p.deleted_at IS NULL", id) - - if err := query.First(&Partner).Error; err != nil { - logger.ContextLogger(ctx).Error("error when get by id Partner", zap.Error(err)) - return nil, err - } - - Partner.Partner.Balance = Partner.WalletBalance - Partner.Partner.AdminName = Partner.AdminName - Partner.Partner.AdminPhoneNumber = Partner.AdminPhoneNumber - Partner.Partner.AdminEmail = Partner.AdminEmail - - return &entity.PartnerDB{Partner.Partner}, nil -} - -func (b *PartnerRepository) GetAll(ctx context.Context, req entity.PartnerSearch) (entity.PartnerList, int, error) { - var partners []*entity.PartnerDBSearch - var total int64 - - query := b.db.Table("partners p"). - Select("p.*, w.balance as wallet_balance, u.name as admin_name, u.email as admin_email, u.phone_number as admin_phone_number"). - Joins("LEFT JOIN wallets w ON w.partner_id = p.id"). - Joins("LEFT JOIN users u ON p.admin_user_id = u.id"). - Where("p.deleted_at IS NULL") - - if req.Search != "" { - query = query.Where("p.name ILIKE ?", "%"+req.Search+"%") - } - - if req.Name != "" { - query = query.Where("p.name ILIKE ?", "%"+req.Name+"%") - } - - if req.PartnerID != nil { - query = query.Where("p.id = ?", req.PartnerID) - } - - if req.Limit > 0 { - query = query.Limit(req.Limit) - } - - if req.Offset > 0 { - query = query.Offset(req.Offset) - } - - // Find partners with joined wallet balances - if err := query.Find(&partners).Error; err != nil { - logger.ContextLogger(ctx).Error("error when getting all partners", zap.Error(err)) - return nil, 0, err - } - - // Counting total records - if err := b.db.Table("partners p"). - Joins("LEFT JOIN wallets w ON w.partner_id = p.id"). - Where("p.deleted_at IS NULL"). - Count(&total).Error; err != nil { - logger.ContextLogger(ctx).Error("error when counting partners", zap.Error(err)) - return nil, 0, err - } - - partnersSearchResult := entity.PartnerList{} - - for _, partner := range partners { - partner.Balance = partner.WalletBalance - partner.Partner.AdminName = partner.AdminName - partner.Partner.AdminPhoneNumber = partner.AdminPhoneNumber - partner.Partner.AdminEmail = partner.AdminEmail - partnersSearchResult = append(partnersSearchResult, &entity.PartnerDB{partner.Partner}) - } - - return partnersSearchResult, int(total), nil - -} -func (b *PartnerRepository) Delete(ctx context.Context, id int64) error { - Partner := new(entity.PartnerDB) - Partner.ID = id - if err := b.db.Delete(Partner).Error; err != nil { - return err - } - return nil -} diff --git a/internal/repository/payment/payment.go b/internal/repository/payment/payment.go deleted file mode 100644 index 40f51fa..0000000 --- a/internal/repository/payment/payment.go +++ /dev/null @@ -1,84 +0,0 @@ -package payment - -import ( - "context" - "enaklo-pos-be/internal/common/logger" - "enaklo-pos-be/internal/entity" - "github.com/google/uuid" - "go.uber.org/zap" - "gorm.io/gorm" - "strconv" -) - -type PaymentRepository struct { - db *gorm.DB -} - -func NewPaymentRepository(db *gorm.DB) *PaymentRepository { - return &PaymentRepository{ - db: db, - } -} - -func (r *PaymentRepository) Create(ctx context.Context, payment *entity.Payment) (*entity.Payment, error) { - payment.ID = uuid.New() - if err := r.db.WithContext(ctx).Create(payment).Error; err != nil { - logger.ContextLogger(ctx).Error("error when creating payment", zap.Error(err)) - return nil, err - } - return payment, nil -} - -// Update updates an existing payment record in the database -func (r *PaymentRepository) Update(ctx context.Context, payment *entity.Payment) (*entity.Payment, error) { - if err := r.db.WithContext(ctx).Save(payment).Error; err != nil { - logger.ContextLogger(ctx).Error("error when updating payment", zap.Error(err)) - return nil, err - } - return payment, nil -} - -func (r *PaymentRepository) UpdateWithTx(ctx context.Context, tx *gorm.DB, payment *entity.Payment) (*entity.Payment, error) { - if err := tx.WithContext(ctx).Save(payment).Error; err != nil { - logger.ContextLogger(ctx).Error("error when updating payment", zap.Error(err)) - return nil, err - } - return payment, nil -} - -// FindByID retrieves a payment record by its ID -func (r *PaymentRepository) FindByID(ctx context.Context, id uuid.UUID) (*entity.Payment, error) { - payment := new(entity.Payment) - if err := r.db.WithContext(ctx).First(payment, id).Error; err != nil { - logger.ContextLogger(ctx).Error("error when finding payment by ID", zap.Error(err)) - return nil, err - } - return payment, nil -} - -func (r *PaymentRepository) FindByOrderAndPartnerID(ctx context.Context, orderID, partnerID int64) (*entity.Payment, error) { - payment := new(entity.Payment) - orderIDStr := strconv.FormatInt(orderID, 10) - partnerIDStr := strconv.FormatInt(partnerID, 10) - if err := r.db.WithContext(ctx). - Where("order_id = ? AND partner_id = ?", orderIDStr, partnerIDStr). - First(payment).Error; err != nil { - logger.ContextLogger(ctx).Error("error when finding payment by order ID and partner ID", zap.Error(err)) - return nil, err - } - return payment, nil -} - -// FindByReferenceID retrieves a payment record by its reference ID -func (r *PaymentRepository) FindByReferenceID(ctx context.Context, db *gorm.DB, referenceID string) (*entity.Payment, error) { - payment := new(entity.Payment) - if db == nil { - db = r.db - } - - if err := db.WithContext(ctx).Where("reference_id = ?", referenceID).First(payment).Error; err != nil { - logger.ContextLogger(ctx).Error("error when finding payment by reference ID", zap.Error(err)) - return nil, err - } - return payment, nil -} diff --git a/internal/repository/payment_gateway/paymentgateway.go b/internal/repository/payment_gateway/paymentgateway.go deleted file mode 100644 index 73731ea..0000000 --- a/internal/repository/payment_gateway/paymentgateway.go +++ /dev/null @@ -1,136 +0,0 @@ -package pg - -import ( - "enaklo-pos-be/internal/entity" - "enaklo-pos-be/internal/repository/linkqu" - mdtrns "enaklo-pos-be/internal/repository/midtrans" - "fmt" -) - -type PaymentGatewayRepo struct { - midtransService *mdtrns.ClientService - linkquService *linkqu.LinkQuService -} - -func NewPaymentGatewayRepo(midtransConfig mdtrns.MidtransConfig, linkquConfig linkqu.LinkQuConfig) *PaymentGatewayRepo { - return &PaymentGatewayRepo{ - midtransService: mdtrns.New(midtransConfig), - linkquService: linkqu.NewLinkQuService(linkquConfig), - } -} - -func (repo *PaymentGatewayRepo) CreatePayment(request entity.PaymentRequest) (*entity.PaymentResponse, error) { - return repo.createMidtransPayment(request) -} - -func (repo *PaymentGatewayRepo) CreateQRISPayment(request entity.PaymentRequest) (*entity.PaymentResponse, error) { - switch request.Provider { - case "MIDTRANS": - return repo.createMidtransQRISPayment(request) - case "LINKQU": - return repo.createLinkQuQRISPayment(request) - default: - return nil, fmt.Errorf("unsupported payment method for QRIS: %s", request.Provider) - } -} - -func (repo *PaymentGatewayRepo) CreatePaymentVA(request entity.PaymentRequest) (*entity.PaymentResponse, error) { - resp, err := repo.linkquService.CreatePaymentVA(entity.LinkQuRequest{ - TotalAmount: request.TotalAmount, - PaymentReferenceID: request.PaymentReferenceID, - CustomerID: request.CustomerID, - CustomerName: request.CustomerName, - CustomerEmail: request.CustomerEmail, - BankCode: request.BankCode, - }) - - if err != nil { - return nil, err - } - - return &entity.PaymentResponse{ - VirtualAccountNumber: resp.VirtualAccount, - BankName: resp.BankName, - BankCode: resp.BankCode, - }, nil -} - -func (repo *PaymentGatewayRepo) createMidtransPayment(request entity.PaymentRequest) (*entity.PaymentResponse, error) { - midtransReq := entity.MidtransRequest{ - PaymentReferenceID: request.PaymentReferenceID, - PaymentMethod: request.Provider, - TotalAmount: request.TotalAmount, - } - - resp, err := repo.midtransService.CreatePayment(midtransReq) - if err != nil { - return nil, err - } - - return &entity.PaymentResponse{ - Token: resp.Token, - RedirectURL: resp.RedirectURL, - }, nil -} - -func (repo *PaymentGatewayRepo) createLinkQuPayment(request entity.PaymentRequest) (*entity.PaymentResponse, error) { - linkquReq := entity.LinkQuRequest{ - PaymentReferenceID: request.PaymentReferenceID, - TotalAmount: request.TotalAmount, - CustomerID: request.CustomerID, - CustomerName: request.CustomerName, - CustomerPhone: request.CustomerPhone, - CustomerEmail: request.CustomerEmail, - } - - resp, err := repo.linkquService.CreateQrisPayment(linkquReq) - if err != nil { - return nil, err - } - - return &entity.PaymentResponse{ - Token: resp.PartnerReff2, - RedirectURL: resp.ImageQRIS, - }, nil -} - -func (repo *PaymentGatewayRepo) createMidtransQRISPayment(request entity.PaymentRequest) (*entity.PaymentResponse, error) { - midtransReq := entity.MidtransRequest{ - PaymentReferenceID: request.PaymentReferenceID, - PaymentMethod: "QRIS", - TotalAmount: request.TotalAmount, - } - - resp, err := repo.midtransService.CreateQrisPayment(midtransReq) - if err != nil { - return nil, err - } - - return &entity.PaymentResponse{ - QRCodeURL: resp.QrCodeUrl, - OrderID: resp.OrderID, - Amount: resp.Amount, - }, nil -} - -func (repo *PaymentGatewayRepo) createLinkQuQRISPayment(request entity.PaymentRequest) (*entity.PaymentResponse, error) { - linkquReq := entity.LinkQuRequest{ - PaymentReferenceID: request.PaymentReferenceID, - TotalAmount: request.TotalAmount, - CustomerID: request.CustomerID, - CustomerName: request.CustomerName, - CustomerPhone: request.CustomerPhone, - CustomerEmail: request.CustomerEmail, - } - - resp, err := repo.linkquService.CreateQrisPayment(linkquReq) - if err != nil { - return nil, err - } - - return &entity.PaymentResponse{ - QRCodeURL: resp.ImageQRIS, - OrderID: resp.PartnerReff, - Amount: resp.Amount, - }, nil -} diff --git a/internal/repository/payment_method_repository.go b/internal/repository/payment_method_repository.go new file mode 100644 index 0000000..41977b7 --- /dev/null +++ b/internal/repository/payment_method_repository.go @@ -0,0 +1,119 @@ +package repository + +import ( + "context" + + "apskel-pos-be/internal/entities" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type PaymentMethodRepository interface { + Create(ctx context.Context, paymentMethod *entities.PaymentMethod) error + GetByID(ctx context.Context, id uuid.UUID) (*entities.PaymentMethod, error) + GetByOrganizationID(ctx context.Context, organizationID uuid.UUID) ([]*entities.PaymentMethod, error) + GetActiveByOrganizationID(ctx context.Context, organizationID uuid.UUID) ([]*entities.PaymentMethod, error) + Update(ctx context.Context, paymentMethod *entities.PaymentMethod) error + Delete(ctx context.Context, id uuid.UUID) error + List(ctx context.Context, filters map[string]interface{}, limit, offset int) ([]*entities.PaymentMethod, int64, error) + Count(ctx context.Context, filters map[string]interface{}) (int64, error) + ExistsByName(ctx context.Context, organizationID uuid.UUID, name string, excludeID *uuid.UUID) (bool, error) +} + +type PaymentMethodRepositoryImpl struct { + db *gorm.DB +} + +func NewPaymentMethodRepositoryImpl(db *gorm.DB) *PaymentMethodRepositoryImpl { + return &PaymentMethodRepositoryImpl{ + db: db, + } +} + +func (r *PaymentMethodRepositoryImpl) Create(ctx context.Context, paymentMethod *entities.PaymentMethod) error { + return r.db.WithContext(ctx).Create(paymentMethod).Error +} + +func (r *PaymentMethodRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID) (*entities.PaymentMethod, error) { + var paymentMethod entities.PaymentMethod + err := r.db.WithContext(ctx).First(&paymentMethod, "id = ?", id).Error + if err != nil { + return nil, err + } + return &paymentMethod, nil +} + +func (r *PaymentMethodRepositoryImpl) GetByOrganizationID(ctx context.Context, organizationID uuid.UUID) ([]*entities.PaymentMethod, error) { + var paymentMethods []*entities.PaymentMethod + err := r.db.WithContext(ctx).Where("organization_id = ?", organizationID).Find(&paymentMethods).Error + return paymentMethods, err +} + +func (r *PaymentMethodRepositoryImpl) GetActiveByOrganizationID(ctx context.Context, organizationID uuid.UUID) ([]*entities.PaymentMethod, error) { + var paymentMethods []*entities.PaymentMethod + err := r.db.WithContext(ctx).Where("organization_id = ? AND is_active = ?", organizationID, true).Find(&paymentMethods).Error + return paymentMethods, err +} + +func (r *PaymentMethodRepositoryImpl) Update(ctx context.Context, paymentMethod *entities.PaymentMethod) error { + return r.db.WithContext(ctx).Save(paymentMethod).Error +} + +func (r *PaymentMethodRepositoryImpl) Delete(ctx context.Context, id uuid.UUID) error { + return r.db.WithContext(ctx).Delete(&entities.PaymentMethod{}, "id = ?", id).Error +} + +func (r *PaymentMethodRepositoryImpl) List(ctx context.Context, filters map[string]interface{}, limit, offset int) ([]*entities.PaymentMethod, int64, error) { + var paymentMethods []*entities.PaymentMethod + var total int64 + + query := r.db.WithContext(ctx).Model(&entities.PaymentMethod{}) + + for key, value := range filters { + switch key { + case "search": + searchValue := "%" + value.(string) + "%" + query = query.Where("name ILIKE ? OR processor ILIKE ?", searchValue, searchValue) + default: + query = query.Where(key+" = ?", value) + } + } + + if err := query.Count(&total).Error; err != nil { + return nil, 0, err + } + + err := query.Limit(limit).Offset(offset).Find(&paymentMethods).Error + return paymentMethods, total, err +} + +func (r *PaymentMethodRepositoryImpl) Count(ctx context.Context, filters map[string]interface{}) (int64, error) { + var count int64 + query := r.db.WithContext(ctx).Model(&entities.PaymentMethod{}) + + for key, value := range filters { + switch key { + case "search": + searchValue := "%" + value.(string) + "%" + query = query.Where("name ILIKE ? OR processor ILIKE ?", searchValue, searchValue) + default: + query = query.Where(key+" = ?", value) + } + } + + err := query.Count(&count).Error + return count, err +} + +func (r *PaymentMethodRepositoryImpl) ExistsByName(ctx context.Context, organizationID uuid.UUID, name string, excludeID *uuid.UUID) (bool, error) { + var count int64 + query := r.db.WithContext(ctx).Model(&entities.PaymentMethod{}).Where("organization_id = ? AND name = ?", organizationID, name) + + if excludeID != nil { + query = query.Where("id != ?", *excludeID) + } + + err := query.Count(&count).Error + return count > 0, err +} diff --git a/internal/repository/payment_repository.go b/internal/repository/payment_repository.go new file mode 100644 index 0000000..ba2d7a4 --- /dev/null +++ b/internal/repository/payment_repository.go @@ -0,0 +1,94 @@ +package repository + +import ( + "context" + "time" + + "apskel-pos-be/internal/entities" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type PaymentRepository interface { + Create(ctx context.Context, payment *entities.Payment) error + GetByID(ctx context.Context, id uuid.UUID) (*entities.Payment, error) + GetByOrderID(ctx context.Context, orderID uuid.UUID) ([]*entities.Payment, error) + Update(ctx context.Context, payment *entities.Payment) error + Delete(ctx context.Context, id uuid.UUID) error + RefundPayment(ctx context.Context, id uuid.UUID, refundAmount float64, reason string, refundedBy uuid.UUID) error + UpdateStatus(ctx context.Context, id uuid.UUID, status entities.PaymentTransactionStatus) error + GetTotalPaidByOrderID(ctx context.Context, orderID uuid.UUID) (float64, error) +} + +type PaymentRepositoryImpl struct { + db *gorm.DB +} + +func NewPaymentRepositoryImpl(db *gorm.DB) *PaymentRepositoryImpl { + return &PaymentRepositoryImpl{ + db: db, + } +} + +func (r *PaymentRepositoryImpl) Create(ctx context.Context, payment *entities.Payment) error { + return r.db.WithContext(ctx).Create(payment).Error +} + +func (r *PaymentRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID) (*entities.Payment, error) { + var payment entities.Payment + err := r.db.WithContext(ctx). + Preload("PaymentMethod"). + Preload("PaymentOrderItems"). + First(&payment, "id = ?", id).Error + if err != nil { + return nil, err + } + return &payment, nil +} + +func (r *PaymentRepositoryImpl) GetByOrderID(ctx context.Context, orderID uuid.UUID) ([]*entities.Payment, error) { + var payments []*entities.Payment + err := r.db.WithContext(ctx). + Preload("PaymentMethod"). + Preload("PaymentOrderItems"). + Where("order_id = ?", orderID). + Find(&payments).Error + return payments, err +} + +func (r *PaymentRepositoryImpl) Update(ctx context.Context, payment *entities.Payment) error { + return r.db.WithContext(ctx).Save(payment).Error +} + +func (r *PaymentRepositoryImpl) Delete(ctx context.Context, id uuid.UUID) error { + return r.db.WithContext(ctx).Delete(&entities.Payment{}, "id = ?", id).Error +} + +func (r *PaymentRepositoryImpl) RefundPayment(ctx context.Context, id uuid.UUID, refundAmount float64, reason string, refundedBy uuid.UUID) error { + now := time.Now() + return r.db.WithContext(ctx).Model(&entities.Payment{}). + Where("id = ?", id). + Updates(map[string]interface{}{ + "refund_amount": refundAmount, + "refund_reason": reason, + "refunded_at": now, + "refunded_by": refundedBy, + "status": entities.PaymentTransactionStatusRefunded, + }).Error +} + +func (r *PaymentRepositoryImpl) UpdateStatus(ctx context.Context, id uuid.UUID, status entities.PaymentTransactionStatus) error { + return r.db.WithContext(ctx).Model(&entities.Payment{}). + Where("id = ?", id). + Update("status", status).Error +} + +func (r *PaymentRepositoryImpl) GetTotalPaidByOrderID(ctx context.Context, orderID uuid.UUID) (float64, error) { + var total float64 + err := r.db.WithContext(ctx).Model(&entities.Payment{}). + Where("order_id = ? AND status = ?", orderID, entities.PaymentTransactionStatusCompleted). + Select("COALESCE(SUM(amount), 0)"). + Scan(&total).Error + return total, err +} diff --git a/internal/repository/product_repo.go b/internal/repository/product_repo.go deleted file mode 100644 index a639811..0000000 --- a/internal/repository/product_repo.go +++ /dev/null @@ -1,118 +0,0 @@ -package repository - -import ( - "enaklo-pos-be/internal/common/mycontext" - "enaklo-pos-be/internal/entity" - "enaklo-pos-be/internal/repository/models" - "github.com/pkg/errors" - "gorm.io/gorm" -) - -type ProductRepository interface { - GetProductsByIDs(ctx mycontext.Context, ids []int64, partnerID int64) ([]*entity.Product, error) - GetProductDetails(ctx mycontext.Context, productIDs []int64, partnerID int64) (*entity.ProductDetails, error) - GetProductsByPartnerID(ctx mycontext.Context, req entity.ProductSearch) ([]*entity.Product, int64, error) -} - -type productRepository struct { - db *gorm.DB -} - -func NewproductRepository(db *gorm.DB) *productRepository { - return &productRepository{db: db} -} - -func (r *productRepository) GetProductsByIDs(ctx mycontext.Context, ids []int64, partnerID int64) ([]*entity.Product, error) { - if len(ids) == 0 { - return []*entity.Product{}, nil - } - - var productsDB []models.ProductDB - - if err := r.db.Where("id IN ? AND partner_id = ?", ids, partnerID).Find(&productsDB).Error; err != nil { - return nil, errors.Wrap(err, "failed to find products") - } - - products := make([]*entity.Product, 0, len(productsDB)) - for i := range productsDB { - product := r.toDomainProductModel(&productsDB[i]) - products = append(products, product) - } - - return products, nil -} - -func (r *productRepository) GetProductDetails(ctx mycontext.Context, productIDs []int64, partnerID int64) (*entity.ProductDetails, error) { - if len(productIDs) == 0 { - return &entity.ProductDetails{ - Products: make(map[int64]*entity.Product), - }, nil - } - - var productsDB []models.ProductDB - - if err := r.db.Where("id IN ? AND partner_id = ?", productIDs, partnerID).Find(&productsDB).Error; err != nil { - return nil, errors.Wrap(err, "failed to find products") - } - - productMap := make(map[int64]*entity.Product, len(productsDB)) - - for i := range productsDB { - product := r.toDomainProductModel(&productsDB[i]) - productMap[product.ID] = product - } - - return &entity.ProductDetails{ - Products: productMap, - PartnerID: partnerID, - }, nil -} - -func (r *productRepository) toDomainProductModel(dbModel *models.ProductDB) *entity.Product { - return &entity.Product{ - ID: dbModel.ID, - PartnerID: dbModel.PartnerID, - Name: dbModel.Name, - Description: dbModel.Description, - Price: dbModel.Price, - Type: dbModel.Type, - Status: dbModel.Status, - CreatedAt: dbModel.CreatedAt, - UpdatedAt: dbModel.UpdatedAt, - Image: dbModel.Image, - } -} - -func (r *productRepository) GetProductsByPartnerID(ctx mycontext.Context, req entity.ProductSearch) ([]*entity.Product, int64, error) { - if req.PartnerID == 0 { - return nil, 0, nil - } - query := r.db.Where("partner_id = ?", req.PartnerID) - - if req.Type != "" { - query = query.Where("type = ?", req.Type) - } - - if req.Name != "" { - query = query.Where("name ILIKE ?", "%"+req.Name+"%") - } - - var total int64 - if err := query.Model(&models.ProductDB{}).Count(&total).Error; err != nil { - return nil, 0, errors.Wrap(err, "failed to count products") - } - - var productsDB []models.ProductDB - - if err := query.Find(&productsDB).Error; err != nil { - return nil, 0, errors.Wrap(err, "failed to find products") - } - - products := make([]*entity.Product, 0, len(productsDB)) - for i := range productsDB { - product := r.toDomainProductModel(&productsDB[i]) - products = append(products, product) - } - - return products, total, nil -} diff --git a/internal/repository/product_repository.go b/internal/repository/product_repository.go new file mode 100644 index 0000000..e0df5f1 --- /dev/null +++ b/internal/repository/product_repository.go @@ -0,0 +1,191 @@ +package repository + +import ( + "context" + + "apskel-pos-be/internal/entities" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type ProductRepositoryImpl struct { + db *gorm.DB +} + +func NewProductRepositoryImpl(db *gorm.DB) *ProductRepositoryImpl { + return &ProductRepositoryImpl{ + db: db, + } +} + +func (r *ProductRepositoryImpl) Create(ctx context.Context, product *entities.Product) error { + return r.db.WithContext(ctx).Create(product).Error +} + +func (r *ProductRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID) (*entities.Product, error) { + var product entities.Product + err := r.db.WithContext(ctx).First(&product, "id = ?", id).Error + if err != nil { + return nil, err + } + return &product, nil +} + +func (r *ProductRepositoryImpl) GetWithCategory(ctx context.Context, id uuid.UUID) (*entities.Product, error) { + var product entities.Product + err := r.db.WithContext(ctx).Preload("Category").Preload("ProductVariants").First(&product, "id = ?", id).Error + if err != nil { + return nil, err + } + return &product, nil +} + +func (r *ProductRepositoryImpl) GetWithRelations(ctx context.Context, id uuid.UUID) (*entities.Product, error) { + var product entities.Product + err := r.db.WithContext(ctx). + Preload("Category"). + Preload("ProductVariants"). + Preload("Inventory"). + First(&product, "id = ?", id).Error + if err != nil { + return nil, err + } + return &product, nil +} + +func (r *ProductRepositoryImpl) GetByOrganization(ctx context.Context, organizationID uuid.UUID) ([]*entities.Product, error) { + var products []*entities.Product + err := r.db.WithContext(ctx).Where("organization_id = ?", organizationID).Find(&products).Error + return products, err +} + +func (r *ProductRepositoryImpl) GetByCategory(ctx context.Context, categoryID uuid.UUID) ([]*entities.Product, error) { + var products []*entities.Product + err := r.db.WithContext(ctx).Where("category_id = ?", categoryID).Find(&products).Error + return products, err +} + +func (r *ProductRepositoryImpl) GetByBusinessType(ctx context.Context, businessType string) ([]*entities.Product, error) { + var products []*entities.Product + err := r.db.WithContext(ctx).Where("business_type = ?", businessType).Find(&products).Error + return products, err +} + +func (r *ProductRepositoryImpl) GetActiveByCategoryID(ctx context.Context, categoryID uuid.UUID) ([]*entities.Product, error) { + var products []*entities.Product + err := r.db.WithContext(ctx).Where("category_id = ? AND is_active = ?", categoryID, true).Find(&products).Error + return products, err +} + +func (r *ProductRepositoryImpl) Update(ctx context.Context, product *entities.Product) error { + return r.db.WithContext(ctx).Save(product).Error +} + +func (r *ProductRepositoryImpl) Delete(ctx context.Context, id uuid.UUID) error { + return r.db.WithContext(ctx).Delete(&entities.Product{}, "id = ?", id).Error +} + +func (r *ProductRepositoryImpl) List(ctx context.Context, filters map[string]interface{}, limit, offset int) ([]*entities.Product, int64, error) { + var products []*entities.Product + var total int64 + + query := r.db.WithContext(ctx).Model(&entities.Product{}).Preload("Category").Preload("ProductVariants") + + for key, value := range filters { + switch key { + case "search": + searchValue := "%" + value.(string) + "%" + query = query.Where("name ILIKE ? OR description ILIKE ? OR sku ILIKE ?", searchValue, searchValue, searchValue) + case "price_min": + query = query.Where("price >= ?", value) + case "price_max": + query = query.Where("price <= ?", value) + default: + query = query.Where(key+" = ?", value) + } + } + + if err := query.Count(&total).Error; err != nil { + return nil, 0, err + } + + err := query.Limit(limit).Offset(offset).Find(&products).Error + return products, total, err +} + +func (r *ProductRepositoryImpl) Count(ctx context.Context, filters map[string]interface{}) (int64, error) { + var count int64 + query := r.db.WithContext(ctx).Model(&entities.Product{}) + + for key, value := range filters { + switch key { + case "search": + searchValue := "%" + value.(string) + "%" + query = query.Where("name ILIKE ? OR description ILIKE ? OR sku ILIKE ?", searchValue, searchValue, searchValue) + case "price_min": + query = query.Where("price >= ?", value) + case "price_max": + query = query.Where("price <= ?", value) + default: + query = query.Where(key+" = ?", value) + } + } + + err := query.Count(&count).Error + return count, err +} + +func (r *ProductRepositoryImpl) GetBySKU(ctx context.Context, organizationID uuid.UUID, sku string) (*entities.Product, error) { + var product entities.Product + err := r.db.WithContext(ctx).Where("organization_id = ? AND sku = ?", organizationID, sku).First(&product).Error + if err != nil { + return nil, err + } + return &product, nil +} + +func (r *ProductRepositoryImpl) ExistsBySKU(ctx context.Context, organizationID uuid.UUID, sku string, excludeID *uuid.UUID) (bool, error) { + query := r.db.WithContext(ctx).Model(&entities.Product{}).Where("organization_id = ? AND sku = ?", organizationID, sku) + + if excludeID != nil { + query = query.Where("id != ?", *excludeID) + } + + var count int64 + err := query.Count(&count).Error + return count > 0, err +} + +func (r *ProductRepositoryImpl) GetByName(ctx context.Context, organizationID uuid.UUID, name string) (*entities.Product, error) { + var product entities.Product + err := r.db.WithContext(ctx).Where("organization_id = ? AND name = ?", organizationID, name).First(&product).Error + if err != nil { + return nil, err + } + return &product, nil +} + +func (r *ProductRepositoryImpl) ExistsByName(ctx context.Context, organizationID uuid.UUID, name string, excludeID *uuid.UUID) (bool, error) { + query := r.db.WithContext(ctx).Model(&entities.Product{}).Where("organization_id = ? AND name = ?", organizationID, name) + + if excludeID != nil { + query = query.Where("id != ?", *excludeID) + } + + var count int64 + err := query.Count(&count).Error + return count > 0, err +} + +func (r *ProductRepositoryImpl) UpdateActiveStatus(ctx context.Context, id uuid.UUID, isActive bool) error { + return r.db.WithContext(ctx).Model(&entities.Product{}). + Where("id = ?", id). + Update("is_active", isActive).Error +} + +func (r *ProductRepositoryImpl) GetLowCostProducts(ctx context.Context, organizationID uuid.UUID, maxCost float64) ([]*entities.Product, error) { + var products []*entities.Product + err := r.db.WithContext(ctx).Where("organization_id = ? AND cost <= ? AND is_active = ?", organizationID, maxCost, true).Find(&products).Error + return products, err +} diff --git a/internal/repository/product_variant_repository.go b/internal/repository/product_variant_repository.go new file mode 100644 index 0000000..40d04cc --- /dev/null +++ b/internal/repository/product_variant_repository.go @@ -0,0 +1,78 @@ +package repository + +import ( + "context" + + "apskel-pos-be/internal/entities" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type ProductVariantRepository interface { + Create(ctx context.Context, variant *entities.ProductVariant) error + GetByID(ctx context.Context, id uuid.UUID) (*entities.ProductVariant, error) + GetByProductID(ctx context.Context, productID uuid.UUID) ([]*entities.ProductVariant, error) + GetByProductAndName(ctx context.Context, productID uuid.UUID, name string) (*entities.ProductVariant, error) + Update(ctx context.Context, variant *entities.ProductVariant) error + Delete(ctx context.Context, id uuid.UUID) error + ExistsByName(ctx context.Context, productID uuid.UUID, name string, excludeID *uuid.UUID) (bool, error) +} + +type ProductVariantRepositoryImpl struct { + db *gorm.DB +} + +func NewProductVariantRepositoryImpl(db *gorm.DB) *ProductVariantRepositoryImpl { + return &ProductVariantRepositoryImpl{ + db: db, + } +} + +func (r *ProductVariantRepositoryImpl) Create(ctx context.Context, variant *entities.ProductVariant) error { + return r.db.WithContext(ctx).Create(variant).Error +} + +func (r *ProductVariantRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID) (*entities.ProductVariant, error) { + var variant entities.ProductVariant + err := r.db.WithContext(ctx).First(&variant, "id = ?", id).Error + if err != nil { + return nil, err + } + return &variant, nil +} + +func (r *ProductVariantRepositoryImpl) GetByProductID(ctx context.Context, productID uuid.UUID) ([]*entities.ProductVariant, error) { + var variants []*entities.ProductVariant + err := r.db.WithContext(ctx).Where("product_id = ?", productID).Find(&variants).Error + return variants, err +} + +func (r *ProductVariantRepositoryImpl) GetByProductAndName(ctx context.Context, productID uuid.UUID, name string) (*entities.ProductVariant, error) { + var variant entities.ProductVariant + err := r.db.WithContext(ctx).Where("product_id = ? AND name = ?", productID, name).First(&variant).Error + if err != nil { + return nil, err + } + return &variant, nil +} + +func (r *ProductVariantRepositoryImpl) Update(ctx context.Context, variant *entities.ProductVariant) error { + return r.db.WithContext(ctx).Save(variant).Error +} + +func (r *ProductVariantRepositoryImpl) Delete(ctx context.Context, id uuid.UUID) error { + return r.db.WithContext(ctx).Delete(&entities.ProductVariant{}, "id = ?", id).Error +} + +func (r *ProductVariantRepositoryImpl) ExistsByName(ctx context.Context, productID uuid.UUID, name string, excludeID *uuid.UUID) (bool, error) { + var count int64 + query := r.db.WithContext(ctx).Model(&entities.ProductVariant{}).Where("product_id = ? AND name = ?", productID, name) + + if excludeID != nil { + query = query.Where("id != ?", *excludeID) + } + + err := query.Count(&count).Error + return count > 0, err +} diff --git a/internal/repository/products/product.go b/internal/repository/products/product.go deleted file mode 100644 index fe82abe..0000000 --- a/internal/repository/products/product.go +++ /dev/null @@ -1,132 +0,0 @@ -package products - -import ( - "context" - "enaklo-pos-be/internal/common/logger" - "enaklo-pos-be/internal/entity" - - "go.uber.org/zap" - "gorm.io/gorm" -) - -type ProductRepository struct { - db *gorm.DB -} - -func NewProductRepository(db *gorm.DB) *ProductRepository { - return &ProductRepository{ - db: db, - } -} - -func (b *ProductRepository) CreateProduct(ctx context.Context, product *entity.ProductDB) (*entity.ProductDB, error) { - err := b.db.Create(product).Error - if err != nil { - logger.ContextLogger(ctx).Error("error when create product", zap.Error(err)) - return nil, err - } - return product, nil -} - -func (b *ProductRepository) UpdateProduct(ctx context.Context, product *entity.ProductDB) (*entity.ProductDB, error) { - if err := b.db.Save(product).Error; err != nil { - logger.ContextLogger(ctx).Error("error when update product", zap.Error(err)) - return nil, err - } - return product, nil -} - -func (b *ProductRepository) GetProductByID(ctx context.Context, id int64) (*entity.ProductDB, error) { - product := new(entity.ProductDB) - if err := b.db.Preload("Category").First(product, id).Error; err != nil { - logger.ContextLogger(ctx).Error("error when get by id product", zap.Error(err)) - return nil, err - } - return product, nil -} - -func (b *ProductRepository) GetProductByPartnerIDAndSiteID(ctx context.Context, partnerID, siteID int64) (entity.ProductList, error) { - var products []*entity.ProductDB - if err := b.db.WithContext(ctx).Where("partner_id = ? AND site_id = ?", partnerID, siteID).Find(&products).Error; err != nil { - logger.ContextLogger(ctx).Error("error when finding product by partner ID and site id", zap.Error(err)) - return nil, err - } - - return products, nil -} - -func (b *ProductRepository) GetProductsBySiteID(ctx context.Context, siteID int64) (entity.ProductList, error) { - var products []*entity.ProductDB - if err := b.db.WithContext(ctx).Where("site_id = ?", siteID).Find(&products).Error; err != nil { - logger.ContextLogger(ctx).Error("error when finding product by partner ID and site id", zap.Error(err)) - return nil, err - } - - return products, nil -} - -func (b *ProductRepository) GetAllProducts(ctx context.Context, req entity.ProductSearch) (entity.ProductList, int, error) { - var products []*entity.ProductDB - var total int64 - - query := b.db - query = query.Where("deleted_at is null") - - if req.Search != "" { - query = query.Where("name ILIKE ?", "%"+req.Search+"%") - } - - if req.Name != "" { - query = query.Where("name ILIKE ?", "%"+req.Name+"%") - } - - if req.Type != "" { - query = query.Where("type = ? ", req.Type) - } - - if req.CategoryID > 0 { - query = query.Where("category_id = ? ", req.CategoryID) - } - - if req.PartnerID > 0 { - query = query.Where("partner_id = ? ", req.PartnerID) - } - - if req.Limit > 0 { - query = query.Limit(req.Limit) - } - - if req.Offset > 0 { - query = query.Offset(req.Offset) - } - - if err := query.Preload("Category").Find(&products).Order("id ASC").Error; err != nil { - logger.ContextLogger(ctx).Error("error when get all products", zap.Error(err)) - return nil, 0, err - } - - if err := b.db.Model(&entity.ProductDB{}).Where(query).Count(&total).Error; err != nil { - logger.ContextLogger(ctx).Error("error when count products", zap.Error(err)) - return nil, 0, err - } - - return products, int(total), nil -} - -func (b *ProductRepository) DeleteProduct(ctx context.Context, id int64) error { - product := new(entity.ProductDB) - product.ID = id - if err := b.db.Delete(product).Error; err != nil { - return err - } - return nil -} - -func (b *ProductRepository) GetProductsByIDs(ctx context.Context, ids []int64, partnerID int64) ([]*entity.ProductDB, error) { - var products []*entity.ProductDB - if err := b.db.Where("partner_id = ? AND id IN ?", partnerID, ids).Find(&products).Error; err != nil { - logger.ContextLogger(ctx).Error("error when getting products by IDs and partner ID", zap.Error(err)) - return nil, err - } - return products, nil -} diff --git a/internal/repository/query_builder.go b/internal/repository/query_builder.go deleted file mode 100644 index e161c08..0000000 --- a/internal/repository/query_builder.go +++ /dev/null @@ -1,193 +0,0 @@ -package repository - -import ( - "github.com/pkg/errors" - "gorm.io/gorm" -) - -type QueryBuilder[T any] struct { - db *gorm.DB - model T -} - -func NewQueryBuilder[T any](db *gorm.DB) *QueryBuilder[T] { - var model T - return &QueryBuilder[T]{ - db: db, - model: model, - } -} - -type Filter struct { - Field string - Operator string // "=", "!=", ">", "<", ">=", "<=", "LIKE", "IN", "NOT IN", "IS NULL", "IS NOT NULL" - Value interface{} -} - -type QueryOptions struct { - Filters []Filter - Limit int - Offset int - OrderBy []string - Preloads []string - GroupBy []string - Having []Filter - Distinct []string - CountOnly bool -} - -func (qb *QueryBuilder[T]) BuildQuery(options QueryOptions) *gorm.DB { - query := qb.db.Model(&qb.model) - - for _, filter := range options.Filters { - query = qb.applyFilter(query, filter) - } - - if len(options.Distinct) > 0 { - for _, distinct := range options.Distinct { - query = query.Distinct(distinct) - } - } - - if len(options.GroupBy) > 0 { - for _, groupBy := range options.GroupBy { - query = query.Group(groupBy) - } - } - - for _, having := range options.Having { - query = qb.applyFilter(query, having) - } - - return query -} - -func (qb *QueryBuilder[T]) applyFilter(query *gorm.DB, filter Filter) *gorm.DB { - switch filter.Operator { - case "=", "": - return query.Where(filter.Field+" = ?", filter.Value) - case "!=": - return query.Where(filter.Field+" != ?", filter.Value) - case ">": - return query.Where(filter.Field+" > ?", filter.Value) - case "<": - return query.Where(filter.Field+" < ?", filter.Value) - case ">=": - return query.Where(filter.Field+" >= ?", filter.Value) - case "<=": - return query.Where(filter.Field+" <= ?", filter.Value) - case "LIKE": - return query.Where(filter.Field+" LIKE ?", filter.Value) - case "IN": - return query.Where(filter.Field+" IN ?", filter.Value) - case "NOT IN": - return query.Where(filter.Field+" NOT IN ?", filter.Value) - case "IS NULL": - return query.Where(filter.Field + " IS NULL") - case "IS NOT NULL": - return query.Where(filter.Field + " IS NOT NULL") - case "BETWEEN": - if values, ok := filter.Value.([]interface{}); ok && len(values) == 2 { - return query.Where(filter.Field+" BETWEEN ? AND ?", values[0], values[1]) - } - return query - default: - return query.Where(filter.Field+" = ?", filter.Value) - } -} - -func (qb *QueryBuilder[T]) ExecuteQuery(baseQuery *gorm.DB, options QueryOptions) *gorm.DB { - query := baseQuery.Session(&gorm.Session{}) - - if len(options.OrderBy) > 0 { - for _, orderBy := range options.OrderBy { - query = query.Order(orderBy) - } - } - - for _, preload := range options.Preloads { - query = query.Preload(preload) - } - - if options.Limit > 0 { - query = query.Limit(options.Limit) - } - - if options.Offset > 0 { - query = query.Offset(options.Offset) - } - - return query -} - -func (qb *QueryBuilder[T]) Count(baseQuery *gorm.DB) (int64, error) { - var count int64 - if err := baseQuery.Count(&count).Error; err != nil { - return 0, errors.Wrap(err, "failed to count records") - } - return count, nil -} - -func (qb *QueryBuilder[T]) Find(query *gorm.DB) ([]T, error) { - var results []T - if err := query.Find(&results).Error; err != nil { - return nil, errors.Wrap(err, "failed to find records") - } - return results, nil -} - -func (qb *QueryBuilder[T]) First(query *gorm.DB) (*T, error) { - var result T - if err := query.First(&result).Error; err != nil { - return nil, errors.Wrap(err, "failed to find record") - } - return &result, nil -} - -func Equal(field string, value interface{}) Filter { - return Filter{Field: field, Operator: "=", Value: value} -} - -func NotEqual(field string, value interface{}) Filter { - return Filter{Field: field, Operator: "!=", Value: value} -} - -func GreaterThan(field string, value interface{}) Filter { - return Filter{Field: field, Operator: ">", Value: value} -} - -func LessThan(field string, value interface{}) Filter { - return Filter{Field: field, Operator: "<", Value: value} -} - -func GreaterEqual(field string, value interface{}) Filter { - return Filter{Field: field, Operator: ">=", Value: value} -} - -func LessEqual(field string, value interface{}) Filter { - return Filter{Field: field, Operator: "<=", Value: value} -} - -func Like(field string, value string) Filter { - return Filter{Field: field, Operator: "LIKE", Value: value} -} - -func In(field string, values interface{}) Filter { - return Filter{Field: field, Operator: "IN", Value: values} -} - -func NotIn(field string, values interface{}) Filter { - return Filter{Field: field, Operator: "NOT IN", Value: values} -} - -func IsNull(field string) Filter { - return Filter{Field: field, Operator: "IS NULL"} -} - -func IsNotNull(field string) Filter { - return Filter{Field: field, Operator: "IS NOT NULL"} -} - -func Between(field string, start, end interface{}) Filter { - return Filter{Field: field, Operator: "BETWEEN", Value: []interface{}{start, end}} -} diff --git a/internal/repository/repository.go b/internal/repository/repository.go deleted file mode 100644 index 65bb26b..0000000 --- a/internal/repository/repository.go +++ /dev/null @@ -1,245 +0,0 @@ -package repository - -import ( - "context" - "database/sql" - "enaklo-pos-be/internal/common/mycontext" - "enaklo-pos-be/internal/repository/brevo" - "enaklo-pos-be/internal/repository/license" - "enaklo-pos-be/internal/repository/linkqu" - mdtrns "enaklo-pos-be/internal/repository/midtrans" - "enaklo-pos-be/internal/repository/orders" - "enaklo-pos-be/internal/repository/oss" - "enaklo-pos-be/internal/repository/partners" - "enaklo-pos-be/internal/repository/payment" - pg "enaklo-pos-be/internal/repository/payment_gateway" - "enaklo-pos-be/internal/repository/products" - "enaklo-pos-be/internal/repository/sites" - transactions "enaklo-pos-be/internal/repository/transaction" - "enaklo-pos-be/internal/repository/users" - repository "enaklo-pos-be/internal/repository/wallet" - - "github.com/golang-jwt/jwt" - "gorm.io/gorm" - - "enaklo-pos-be/config" - "enaklo-pos-be/internal/entity" - "enaklo-pos-be/internal/repository/auth" - "enaklo-pos-be/internal/repository/crypto" -) - -type RepoManagerImpl struct { - Crypto Crypto - Auth Auth - User User - Product Product - Order Order - OSS OSSRepository - Partner PartnerRepository - Site SiteRepository - Trx Trx - Wallet WalletRepository - Midtrans Midtrans - Payment Payment - EmailService EmailService - License License - Transaction TransactionRepository - PG PaymentGateway - LinkQu LinkQu - - OrderRepo OrderRepository - InProgressOrderRepo InProgressOrderRepository - CustomerRepo CustomerRepo - ProductRepo ProductRepository - TransactionRepo TransactionRepo - MemberRepository MemberRepository - PartnerSetting PartnerSettingsRepository - UndianRepository UndianRepo - CashierSeasionRepo CashierSessionRepository - CategoryRepository CategoryRepository -} - -func NewRepoManagerImpl(db *gorm.DB, cfg *config.Config) *RepoManagerImpl { - return &RepoManagerImpl{ - Crypto: crypto.NewCrypto(cfg.Auth()), - Auth: auth.NewAuthRepository(db), - User: users.NewUserRepository(db), - Product: products.NewProductRepository(db), - Order: orders.NewOrderRepository(db), - OSS: oss.NewOssRepositoryImpl(cfg.OSSConfig), - Partner: partners.NewPartnerRepository(db), - Site: sites.NewSiteRepository(db), - Trx: NewTransactionManager(db), - Wallet: repository.NewWalletRepository(db), - Midtrans: mdtrns.New(&cfg.Midtrans), - Payment: payment.NewPaymentRepository(db), - EmailService: brevo.New(&cfg.Brevo), - License: license.NewLicenseRepository(db), - Transaction: transactions.NewTransactionRepository(db), - PG: pg.NewPaymentGatewayRepo(&cfg.Midtrans, &cfg.LinkQu), - LinkQu: linkqu.NewLinkQuService(&cfg.LinkQu), - - OrderRepo: NeworderRepository(db), - CustomerRepo: NewCustomerRepository(db), - ProductRepo: NewproductRepository(db), - TransactionRepo: NewTransactionRepository(db), - MemberRepository: NewMemberRepository(db), - InProgressOrderRepo: NewInProgressOrderRepository(db), - PartnerSetting: NewPartnerSettingsRepository(db), - UndianRepository: NewUndianRepository(db), - CashierSeasionRepo: NewCashierSessionRepository(db), - CategoryRepository: NewCategoryRepository(db), - } -} - -type Auth interface { - CheckExistsUserAccount(ctx context.Context, email string) (*entity.UserDB, error) - CheckExistsUserAccountByID(ctx context.Context, userID int64) (*entity.UserDB, error) - UpdatePassword(ctx context.Context, trx *gorm.DB, newHashedPassword string, userID int64, resetPassword bool) error -} - -type Crypto interface { - CompareHashAndPassword(hash string, password string) bool - ValidateWT(tokenString string) (*jwt.Token, error) - GenerateJWT(user *entity.User) (string, error) - GenerateJWTReseetPassword(user *entity.User) (string, error) - GenerateJWTOrder(order *entity.Order) (string, error) - GenerateJWTOrderInquiry(inquiry *entity.OrderInquiry) (string, error) - ValidateJWTOrderInquiry(tokenString string) (int64, string, error) - ValidateJWTOrder(tokenString string) (int64, int64, error) - ValidateResetPassword(tokenString string) (int64, error) - ParseAndValidateJWT(token string) (*entity.JWTAuthClaims, error) - GenerateJWTWithdraw(req *entity.WalletWithdrawRequest) (string, error) - ValidateJWTWithdraw(tokenString string) (*entity.WalletWithdrawRequest, error) - GenerateJWTCustomer(user *entity.Customer) (string, error) - ParseAndValidateJWTCustomer(tokenString string) (*entity.JWTAuthClaimsCustomer, error) -} - -type User interface { - Create(ctx context.Context, user *entity.UserDB) (*entity.UserDB, error) - CreateWithTx(ctx context.Context, tx *gorm.DB, user *entity.UserDB) (*entity.UserDB, error) - GetAllUsers(ctx context.Context, req entity.UserSearch) (entity.UserList, int, error) - GetAllCustomer(ctx context.Context, req entity.CustomerSearch) (entity.CustomerList, int, error) - GetUserByID(ctx context.Context, id int64) (*entity.UserDB, error) - GetPartnerAdmin(ctx context.Context, partnerID int64) (*entity.UserDB, error) - GetUserByEmail(ctx context.Context, email string) (*entity.UserDB, error) - UpdateUser(ctx context.Context, user *entity.UserDB) (*entity.UserDB, error) - UpdateUserWithTx(ctx context.Context, tx *gorm.DB, user *entity.UserDB) (*entity.UserDB, error) - CountUsersByRoleAndSiteOrPartner(ctx context.Context, roleID int, siteID *int64) (int, error) -} - -type Studio interface { - CreateStudio(ctx context.Context, studio *entity.StudioDB) (*entity.StudioDB, error) - UpdateStudio(ctx context.Context, studio *entity.StudioDB) (*entity.StudioDB, error) - GetStudioByID(ctx context.Context, id int64) (*entity.StudioDB, error) - SearchStudios(ctx context.Context, req entity.StudioSearch) (entity.StudioList, int, error) -} - -type Product interface { - CreateProduct(ctx context.Context, product *entity.ProductDB) (*entity.ProductDB, error) - UpdateProduct(ctx context.Context, product *entity.ProductDB) (*entity.ProductDB, error) - GetProductByID(ctx context.Context, id int64) (*entity.ProductDB, error) - GetProductByPartnerIDAndSiteID(ctx context.Context, partnerID, siteID int64) (entity.ProductList, error) - GetAllProducts(ctx context.Context, req entity.ProductSearch) (entity.ProductList, int, error) - DeleteProduct(ctx context.Context, id int64) error - GetProductsByIDs(ctx context.Context, ids []int64, partnerID int64) ([]*entity.ProductDB, error) - GetProductsBySiteID(ctx context.Context, siteID int64) (entity.ProductList, error) -} - -type Order interface { - Create(ctx context.Context, order *entity.Order) (*entity.Order, error) - FindByID(ctx context.Context, id int64) (*entity.Order, error) - FindPrintDetailByID(ctx context.Context, id int64) (*entity.OrderPrintDetail, error) - FindByQRCode(ctx context.Context, refID string) (*entity.Order, error) - Update(ctx context.Context, order *entity.Order) (*entity.Order, error) - SetOrderStatus(ctx context.Context, db *gorm.DB, orderID int64, status string) error - GetAllHystoryOrders(ctx context.Context, req entity.OrderSearch) (entity.HistoryOrderList, int, error) - SumAmount(ctx mycontext.Context, req entity.OrderSearch) (*entity.OrderDB, error) - CountSoldOfTicket(ctx mycontext.Context, req entity.OrderSearch) (*entity.TicketSoldDB, error) - GetDailySalesMetrics(ctx context.Context, req entity.OrderSearch) ([]entity.ProductDailySales, error) - GetPaymentTypeDistribution(ctx context.Context, req entity.OrderSearch) ([]entity.PaymentTypeDistribution, error) -} - -type OSSRepository interface { - UploadFile(ctx context.Context, fileName string, fileContent []byte) (fileUrl string, err error) - GetPublicURL(fileName string) string -} - -type PartnerRepository interface { - Create(ctx context.Context, branch *entity.PartnerDB) (*entity.PartnerDB, error) - CreateWithTx(ctx context.Context, tx *gorm.DB, Partner *entity.PartnerDB) (*entity.PartnerDB, error) - UpdateWithTx(ctx context.Context, tx *gorm.DB, Partner *entity.PartnerDB) (*entity.PartnerDB, error) - Update(ctx context.Context, branch *entity.PartnerDB) (*entity.PartnerDB, error) - GetByID(ctx context.Context, id int64) (*entity.PartnerDB, error) - GetAll(ctx context.Context, req entity.PartnerSearch) (entity.PartnerList, int, error) - Delete(ctx context.Context, id int64) error -} - -type SiteRepository interface { - Upsert(ctx context.Context, site *entity.Site) (*entity.Site, error) - Create(ctx context.Context, branch *entity.SiteDB) (*entity.SiteDB, error) - Update(ctx context.Context, branch *entity.SiteDB) (*entity.SiteDB, error) - GetByID(ctx context.Context, id int64) (*entity.SiteDB, error) - GetAll(ctx context.Context, req entity.SiteSearch) (entity.SiteList, int, error) - Delete(ctx context.Context, id int64) error - Count(ctx mycontext.Context, req entity.SiteSearch) (*entity.SiteCountDB, error) - GetNearestSites(ctx context.Context, latitude, longitude, radius float64) ([]entity.SiteProductInfo, error) - SearchSites(ctx context.Context, search *entity.DiscoverySearch) ([]entity.SiteProductInfo, int64, error) -} - -type WalletRepository interface { - Create(ctx context.Context, tx *gorm.DB, wallet *entity.Wallet) (*entity.Wallet, error) - Update(ctx context.Context, db *gorm.DB, wallet *entity.Wallet) (*entity.Wallet, error) - GetByPartnerID(ctx context.Context, db *gorm.DB, partnerID int64) (*entity.Wallet, error) - GetForUpdate(ctx context.Context, tx *gorm.DB, partnerID int64) (*entity.Wallet, error) -} - -type Midtrans interface { - CreatePayment(order entity.MidtransRequest) (*entity.MidtransResponse, error) - CreateQrisPayment(order entity.MidtransRequest) (*entity.MidtransQrisResponse, error) -} - -type Payment interface { - Create(ctx context.Context, payment *entity.Payment) (*entity.Payment, error) - Update(ctx context.Context, payment *entity.Payment) (*entity.Payment, error) - UpdateWithTx(ctx context.Context, tx *gorm.DB, payment *entity.Payment) (*entity.Payment, error) - FindByOrderAndPartnerID(ctx context.Context, orderID, partnerID int64) (*entity.Payment, error) - FindByReferenceID(ctx context.Context, db *gorm.DB, referenceID string) (*entity.Payment, error) -} - -type EmailService interface { - SendEmailTransactional(ctx context.Context, param entity.SendEmailNotificationParam) error -} - -type License interface { - Create(ctx context.Context, license *entity.LicenseDB) (*entity.LicenseDB, error) - Update(ctx context.Context, license *entity.LicenseDB) (*entity.LicenseDB, error) - FindByID(ctx context.Context, id string) (*entity.LicenseDB, error) - GetAll(ctx context.Context, limit, offset int, statusFilter string) ([]*entity.LicenseGetAll, int64, error) - FindByPartnerIDMaxEndDate(ctx context.Context, partnerID *int64) (*entity.LicenseDB, error) -} - -type TransactionRepository interface { - FindByID(ctx context.Context, id string) (*entity.Transaction, error) - Create(ctx context.Context, trx *gorm.DB, transaction *entity.Transaction) (*entity.Transaction, error) - GetTransactionList(ctx mycontext.Context, req entity.TransactionSearch) ([]*entity.TransactionList, int, error) - Update(ctx context.Context, trx *gorm.DB, transaction *entity.Transaction) (*entity.Transaction, error) -} - -type LinkQu interface { - CreateQrisPayment(linkQuRequest entity.LinkQuRequest) (*entity.LinkQuQRISResponse, error) - CreatePaymentVA(linkQuRequest entity.LinkQuRequest) (*entity.LinkQuPaymentVAResponse, error) - CheckPaymentStatus(partnerReff string) (*entity.LinkQuCheckStatusResponse, error) -} - -type PaymentGateway interface { - CreatePayment(request entity.PaymentRequest) (*entity.PaymentResponse, error) - CreateQRISPayment(request entity.PaymentRequest) (*entity.PaymentResponse, error) - CreatePaymentVA(request entity.PaymentRequest) (*entity.PaymentResponse, error) -} - -type Trx interface { - Begin(ctx context.Context, opts ...*sql.TxOptions) (*gorm.DB, error) - Commit(session *gorm.DB) *gorm.DB - Rollback(session *gorm.DB) *gorm.DB -} diff --git a/internal/repository/sites/sites.go b/internal/repository/sites/sites.go deleted file mode 100644 index 1573396..0000000 --- a/internal/repository/sites/sites.go +++ /dev/null @@ -1,282 +0,0 @@ -package sites - -import ( - "context" - "enaklo-pos-be/internal/common/logger" - "enaklo-pos-be/internal/common/mycontext" - "enaklo-pos-be/internal/entity" - - "go.uber.org/zap" - "gorm.io/gorm" -) - -type SiteRepository struct { - db *gorm.DB -} - -func NewSiteRepository(db *gorm.DB) *SiteRepository { - return &SiteRepository{ - db: db, - } -} - -func (r *SiteRepository) Upsert(ctx context.Context, site *entity.Site) (*entity.Site, error) { - err := r.db.Transaction(func(tx *gorm.DB) error { - if site.ID != 0 { - // Update site - if err := tx.Save(site).Error; err != nil { - return err - } - } else { - // Create new site - if err := tx.Create(site).Error; err != nil { - return err - } - } - - if len(site.Products) > 0 { - for i := range site.Products { - if site.Products[i].ID != 0 { - if err := tx.Save(&site.Products[i]).Error; err != nil { - return err - } - } else { - if err := tx.Create(&site.Products[i]).Error; err != nil { - return err - } - } - } - } - return nil - }) - - if err != nil { - logger.ContextLogger(ctx).Error("error when upserting site", zap.Error(err)) - return nil, err - } - return site, nil -} - -func (r *SiteRepository) Create(ctx context.Context, site *entity.SiteDB) (*entity.SiteDB, error) { - err := r.db.Create(site).Error - if err != nil { - logger.ContextLogger(ctx).Error("error when creating site", zap.Error(err)) - return nil, err - } - return site, nil -} - -func (r *SiteRepository) Update(ctx context.Context, site *entity.SiteDB) (*entity.SiteDB, error) { - if err := r.db.Save(site).Error; err != nil { - logger.ContextLogger(ctx).Error("error when updating site", zap.Error(err)) - return nil, err - } - return site, nil -} - -func (r *SiteRepository) GetByID(ctx context.Context, id int64) (*entity.SiteDB, error) { - site := new(entity.SiteDB) - if err := r.db.Preload("Products").First(site, id).Error; err != nil { - logger.ContextLogger(ctx).Error("error when getting site by ID", zap.Error(err)) - return nil, err - } - - return site, nil -} - -func (r *SiteRepository) GetAll(ctx context.Context, req entity.SiteSearch) (entity.SiteList, int, error) { - var sites []*entity.SiteDB - var total int64 - - query := r.db - query = query.Where("deleted_at IS NULL") - - if req.Status != "" { - query = query.Where("status = ?", req.Status) - } - - if req.Search != "" { - query = query.Where("name ILIKE ?", "%"+req.Search+"%") - } - - if req.Name != "" { - query = query.Where("name ILIKE ?", "%"+req.Name+"%") - } - - if req.PartnerID != nil { - query = query.Where("partner_id = ?", req.PartnerID) - } - - if req.SiteID != nil { - query = query.Where("id = ?", req.SiteID) - } - - if req.Limit > 0 { - query = query.Limit(req.Limit) - } - - if req.Offset > 0 { - query = query.Offset(req.Offset) - } - - if err := query.Find(&sites).Error; err != nil { - logger.ContextLogger(ctx).Error("error when getting all sites", zap.Error(err)) - return nil, 0, err - } - - if err := r.db.Model(&entity.SiteDB{}).Where(query).Count(&total).Error; err != nil { - logger.ContextLogger(ctx).Error("error when counting sites", zap.Error(err)) - return nil, 0, err - } - - return sites, int(total), nil -} - -func (r *SiteRepository) Delete(ctx context.Context, id int64) error { - site := new(entity.SiteDB) - site.ID = id - if err := r.db.Delete(site).Error; err != nil { - return err - } - return nil -} - -func (r *SiteRepository) Count(ctx mycontext.Context, req entity.SiteSearch) (*entity.SiteCountDB, error) { - count := new(entity.SiteCountDB) - - query := r.db.Table("sites"). - Select("count(*) as count") - - if !req.IsAdmin { - query = query.Where("partner_id = ?", req.PartnerID) - } - - if err := query.Scan(&count).Error; err != nil { - logger.ContextLogger(ctx).Error("error when get count sites", zap.Error(err)) - return nil, err - } - - return count, nil -} - -func (r *SiteRepository) GetNearestSites(ctx context.Context, latitude, longitude, radius float64) ([]entity.SiteProductInfo, error) { - const limit = 5 - var siteProducts []entity.SiteProductInfo - - distanceQuery := ` - (6371 * acos(cos(radians(?)) * cos(radians(latitude)) * cos(radians(longitude) - radians(?)) + sin(radians(?)) * sin(radians(latitude)))) - ` - - // Primary query for sites within the radius - err := r.db.WithContext(ctx).Raw(` - SELECT s.id AS site_id, s.name AS site_name, s.region, s.regency, s.partner_id, s.image, s.address, s.location_link, s.description, - s.highlight, s.contact_person, s.tnc, s.additional_info, s.status, s.is_season_ticket, s.is_discount_active, - s.latitude, s.longitude, s.created_at, s.updated_at, - `+distanceQuery+` AS distance, - p.id AS product_id, p.name AS product_name, p.type AS product_type, p.price AS product_price, - p.is_weekend_ticket, p.is_season_ticket, p.status AS product_status, p.description AS product_description - FROM sites s - LEFT JOIN ( - SELECT *, ROW_NUMBER() OVER (PARTITION BY site_id ORDER BY price ASC) AS rn - FROM products - ) p ON s.id = p.site_id AND p.rn = 1 - WHERE `+distanceQuery+` < ? and s.deleted_at IS NULL and s.status = 'Active' - ORDER BY distance - LIMIT ?`, - latitude, longitude, latitude, latitude, longitude, latitude, radius, limit).Scan(&siteProducts).Error - - if err != nil { - return nil, err - } - - // If fewer than 5 sites found, fetch additional ones regardless of distance - if len(siteProducts) < limit { - additionalLimit := limit - len(siteProducts) - err = r.db.WithContext(ctx).Raw(` - SELECT s.id AS site_id, s.name AS site_name, s.region, s.regency, s.partner_id, s.image, s.address, s.location_link, s.description, - s.highlight, s.contact_person, s.tnc, s.additional_info, s.status, s.is_season_ticket, s.is_discount_active, - s.latitude, s.longitude, s.created_at, s.updated_at, - `+distanceQuery+` AS distance, - p.id AS product_id, p.name AS product_name, p.type AS product_type, p.price AS product_price, - p.is_weekend_ticket, p.is_season_ticket, p.status AS product_status, p.description AS product_description - FROM sites s - LEFT JOIN ( - SELECT *, ROW_NUMBER() OVER (PARTITION BY site_id ORDER BY price ASC) AS rn - FROM products - ) p ON s.id = p.site_id AND p.rn = 1 - WHERE s.deleted_at IS NULL and s.status = 'Active' - ORDER BY distance - LIMIT ?`, - latitude, longitude, latitude, additionalLimit).Scan(&siteProducts).Error - - if err != nil { - return nil, err - } - } - - return siteProducts, nil -} - -func (r *SiteRepository) SearchSites(ctx context.Context, search *entity.DiscoverySearch) ([]entity.SiteProductInfo, int64, error) { - var siteProducts []entity.SiteProductInfo - var total int64 - - // Adding wildcard for partial matching - searchName := "%" + search.Name + "%" - - // Base conditions and parameters - conditions := "s.name ILIKE ?" - params := []interface{}{searchName} - - // Add region filtering if region is provided - if search.Region != "" { - conditions += " AND s.region = ?" - params = append(params, search.Region) - } - - if search.Status != "" { - conditions += " AND s.status = ?" - params = append(params, search.Status) - } - - if search.Discover != "" { - conditions += " AND s.region ILIKE ?" - params = append(params, "%"+search.Discover+"%") - } - - // Count query to get the total number of matching records - countQuery := ` - SELECT COUNT(*) - FROM sites s - WHERE ` + conditions - err := r.db.WithContext(ctx).Raw(countQuery, params...).Scan(&total).Error - if err != nil { - return nil, 0, err - } - - // Add limit and offset for the data query - dataParams := append(params, search.Limit, search.Offset) - - // Primary query for sites matching name and region, with pagination - dataQuery := ` - SELECT s.id AS site_id, s.name AS site_name, s.region, s.regency, s.partner_id, s.image, s.address, s.location_link, s.description, - s.highlight, s.contact_person, s.tnc, s.additional_info, s.status, s.is_season_ticket, s.is_discount_active, - s.latitude, s.longitude, s.created_at, s.updated_at, - p.id AS product_id, p.name AS product_name, p.type AS product_type, p.price AS product_price, - p.is_weekend_ticket, p.is_season_ticket, p.status AS product_status, p.description AS product_description - FROM sites s - LEFT JOIN ( - SELECT *, ROW_NUMBER() OVER (PARTITION BY site_id ORDER BY price ASC) AS rn - FROM products - ) p ON s.id = p.site_id AND p.rn = 1 - WHERE ` + conditions + ` and s.deleted_at IS NULL and s.status = 'Active' - ORDER BY s.name - LIMIT ? OFFSET ? - ` - err = r.db.WithContext(ctx).Raw(dataQuery, dataParams...).Scan(&siteProducts).Error - if err != nil { - return nil, 0, err - } - - return siteProducts, total, nil -} diff --git a/internal/repository/transaction.go b/internal/repository/transaction.go deleted file mode 100644 index b458b48..0000000 --- a/internal/repository/transaction.go +++ /dev/null @@ -1,32 +0,0 @@ -package repository - -import ( - "context" - "database/sql" - - "gorm.io/gorm" -) - -type TransactionManager struct { - db *gorm.DB -} - -func NewTransactionManager(db *gorm.DB) *TransactionManager { - return &TransactionManager{db: db} -} - -func (tm *TransactionManager) Begin(ctx context.Context, opts ...*sql.TxOptions) (*gorm.DB, error) { - tx := tm.db.Begin(opts...) - if tx.Error != nil { - return nil, tx.Error - } - return tx, nil -} - -func (tm *TransactionManager) Commit(session *gorm.DB) *gorm.DB { - return session.Commit() -} - -func (tm *TransactionManager) Rollback(session *gorm.DB) *gorm.DB { - return session.Rollback() -} diff --git a/internal/repository/transaction/transaction.go b/internal/repository/transaction/transaction.go deleted file mode 100644 index eae6488..0000000 --- a/internal/repository/transaction/transaction.go +++ /dev/null @@ -1,179 +0,0 @@ -package transactions - -import ( - "context" - "enaklo-pos-be/internal/common/mycontext" - "enaklo-pos-be/internal/entity" - "go.uber.org/zap" - "gorm.io/gorm" -) - -// TransactionRepository provides methods to perform CRUD operations on transactions. -type TransactionRepository struct { - db *gorm.DB -} - -func NewTransactionRepository(db *gorm.DB) *TransactionRepository { - return &TransactionRepository{ - db: db, - } -} - -// Create creates a new transaction in the database. -func (r *TransactionRepository) Create(ctx context.Context, trx *gorm.DB, transaction *entity.Transaction) (*entity.Transaction, error) { - // Create the transaction record - if err := trx.WithContext(ctx).Create(transaction).Error; err != nil { - zap.L().Error("error when creating transaction", zap.Error(err)) - return nil, err - } - - // Retrieve the created transaction using the same transaction context - var createdTransaction entity.Transaction - if err := trx.WithContext(ctx).First(&createdTransaction, "id = ?", transaction.ID).Error; err != nil { - zap.L().Error("error when fetching newly created transaction", zap.Error(err)) - return nil, err - } - return &createdTransaction, nil -} - -// Update updates an existing transaction in the database. -func (r *TransactionRepository) Update(ctx context.Context, trx *gorm.DB, transaction *entity.Transaction) (*entity.Transaction, error) { - if err := trx.WithContext(ctx).Save(transaction).Error; err != nil { - zap.L().Error("error when updating transaction", zap.Error(err)) - return nil, err - } - return transaction, nil -} - -func (r *TransactionRepository) FindByID(ctx context.Context, id string) (*entity.Transaction, error) { - var transaction entity.Transaction - if err := r.db.WithContext(ctx).First(&transaction, "id = ?", id).Error; err != nil { - zap.L().Error("error when finding transaction by ID", zap.Error(err)) - return nil, err - } - return &transaction, nil -} - -func (r *TransactionRepository) Delete(ctx context.Context, id string) error { - if err := r.db.WithContext(ctx).Delete(&entity.Transaction{}, "id = ?", id).Error; err != nil { - zap.L().Error("error when deleting transaction", zap.Error(err)) - return err - } - return nil -} - -func (r *TransactionRepository) FindByPartnerID(ctx context.Context, partnerID int64) ([]entity.Transaction, error) { - var transactions []entity.Transaction - if err := r.db.WithContext(ctx).Where("partner_id = ?", partnerID).Find(&transactions).Error; err != nil { - zap.L().Error("error when finding transactions by partner ID", zap.Error(err)) - return nil, err - } - return transactions, nil -} - -func (r *TransactionRepository) FindByStatus(ctx context.Context, status string) ([]entity.Transaction, error) { - var transactions []entity.Transaction - if err := r.db.WithContext(ctx).Where("status = ?", status).Find(&transactions).Error; err != nil { - zap.L().Error("error when finding transactions by status", zap.Error(err)) - return nil, err - } - return transactions, nil -} - -// UpdateStatus updates the status of a transaction by its ID. -func (r *TransactionRepository) UpdateStatus(ctx context.Context, id string, status string) (*entity.Transaction, error) { - transaction, err := r.FindByID(ctx, id) - if err != nil { - return nil, err - } - transaction.Status = status - if err := r.db.WithContext(ctx).Save(transaction).Error; err != nil { - zap.L().Error("error when updating transaction status", zap.Error(err)) - return nil, err - } - return transaction, nil -} - -// ListTransactions retrieves a list of transactions with optional filters for pagination and sorting. -func (r *TransactionRepository) ListTransactions(ctx context.Context, offset int, limit int, status string, transactionType string) ([]entity.Transaction, int64, error) { - var transactions []entity.Transaction - var total int64 - - query := r.db.WithContext(ctx).Model(&entity.Transaction{}).Order("created_at DESC") - - if status != "" { - query = query.Where("status = ?", status) - } - - if transactionType != "" { - query = query.Where("transaction_type = ?", transactionType) - } - - if err := query.Count(&total).Error; err != nil { - zap.L().Error("error when counting transactions", zap.Error(err)) - return nil, 0, err - } - - if offset >= 0 { - query = query.Offset(offset) - } - - if limit > 0 { - query = query.Limit(limit) - } - - if err := query.Find(&transactions).Error; err != nil { - zap.L().Error("error when listing transactions", zap.Error(err)) - return nil, 0, err - } - - return transactions, total, nil -} - -func (r *TransactionRepository) GetTransactionList(ctx mycontext.Context, req entity.TransactionSearch) ([]*entity.TransactionList, int, error) { - var transactions []*entity.TransactionList - var total int64 - - query := r.db.Table("transactions t"). - Select("t.id, t.transaction_type, t.status, t.created_at, s.name as site_name, p.name as partner_name, t.amount, t.fee, t.total"). - Joins("left join sites s on t.site_id = s.id"). - Joins("left join partners p on t.partner_id = p.id") - - if req.SiteID != nil { - query = query.Where("t.site_id = ?", req.SiteID) - } - - if req.Type != "" { - query = query.Where("t.transaction_type = ?", req.Type) - } - - if req.Status != "" { - query = query.Where("t.status = ?", req.Status) - } - - if req.Date != "" { - query = query.Where("DATE(t.created_at) = ?", req.Date) - } - - if req.PartnerID != nil { - query = query.Where("t.partner_id = ?", req.PartnerID) - } - - query = query.Order("t.created_at DESC") - - query = query.Count(&total) - - if req.Offset > 0 { - query = query.Offset(req.Offset) - } - if req.Limit > 0 { - query = query.Limit(req.Limit) - } - - err := query.Find(&transactions).Error - if err != nil { - return nil, 0, err - } - - return transactions, int(total), nil -} diff --git a/internal/repository/transaction_repo.go b/internal/repository/transaction_repo.go deleted file mode 100644 index b2bf4f9..0000000 --- a/internal/repository/transaction_repo.go +++ /dev/null @@ -1,77 +0,0 @@ -package repository - -import ( - "enaklo-pos-be/internal/common/mycontext" - "enaklo-pos-be/internal/entity" - "enaklo-pos-be/internal/repository/models" - "github.com/google/uuid" - "github.com/pkg/errors" - "gorm.io/gorm" -) - -type TransactionRepo interface { - Create(ctx mycontext.Context, transaction *entity.Transaction) (*entity.Transaction, error) - FindByOrderID(ctx mycontext.Context, orderID int64) ([]*entity.Transaction, error) -} - -type transactionRepository struct { - db *gorm.DB -} - -func NewTransactionRepository(db *gorm.DB) *transactionRepository { - return &transactionRepository{ - db: db, - } -} - -func (r *transactionRepository) Create(ctx mycontext.Context, transaction *entity.Transaction) (*entity.Transaction, error) { - transactionDB := r.toTransactionDBModel(transaction) - - if err := r.db.Create(&transactionDB).Error; err != nil { - return nil, errors.Wrap(err, "failed to insert transaction") - } - - return transaction, nil -} - -func (r *transactionRepository) FindByOrderID(ctx mycontext.Context, orderID int64) ([]*entity.Transaction, error) { - var transactionsDB []models.TransactionDB - - if err := r.db.Where("order_id = ?", orderID).Find(&transactionsDB).Error; err != nil { - return nil, errors.Wrap(err, "failed to find transactions for order") - } - - transactions := make([]*entity.Transaction, 0, len(transactionsDB)) - for i := range transactionsDB { - transaction := r.toDomainTransactionModel(&transactionsDB[i]) - transactions = append(transactions, transaction) - } - - return transactions, nil -} - -func (r *transactionRepository) toTransactionDBModel(transaction *entity.Transaction) models.TransactionDB { - return models.TransactionDB{ - ID: uuid.New().String(), - OrderID: transaction.OrderID, - Amount: transaction.Amount, - PaymentMethod: transaction.PaymentMethod, - Status: transaction.Status, - CreatedAt: transaction.CreatedAt, - UpdatedAt: transaction.UpdatedAt, - TransactionType: transaction.TransactionType, - PartnerID: transaction.PartnerID, - } -} - -func (r *transactionRepository) toDomainTransactionModel(dbModel *models.TransactionDB) *entity.Transaction { - return &entity.Transaction{ - ID: dbModel.ID, - OrderID: dbModel.OrderID, - Amount: dbModel.Amount, - PaymentMethod: dbModel.PaymentMethod, - Status: dbModel.Status, - CreatedAt: dbModel.CreatedAt, - UpdatedAt: dbModel.UpdatedAt, - } -} diff --git a/internal/repository/trx/trx.go b/internal/repository/trx/trx.go deleted file mode 100644 index 4c583e6..0000000 --- a/internal/repository/trx/trx.go +++ /dev/null @@ -1,33 +0,0 @@ -package trx - -import ( - "context" - "database/sql" - "gorm.io/gorm" -) - -type GormTransactionManager struct { - db *gorm.DB -} - -func NewGormTransactionManager(db *gorm.DB) *GormTransactionManager { - return &GormTransactionManager{ - db: db, - } -} - -func (tm *GormTransactionManager) Begin(ctx context.Context, opts ...*sql.TxOptions) (*gorm.DB, error) { - tx := tm.db.Begin(opts...) - if tx.Error != nil { - return nil, tx.Error - } - return tx, nil -} - -func (tm *GormTransactionManager) Commit(tx *gorm.DB) *gorm.DB { - return tx.Commit() -} - -func (tm *GormTransactionManager) Rollback(tx *gorm.DB) *gorm.DB { - return tx.Rollback() -} diff --git a/internal/repository/undian_repo.go b/internal/repository/undian_repo.go deleted file mode 100644 index 0b0a5eb..0000000 --- a/internal/repository/undian_repo.go +++ /dev/null @@ -1,140 +0,0 @@ -package repository - -import ( - "enaklo-pos-be/internal/common/logger" - "enaklo-pos-be/internal/common/mycontext" - "enaklo-pos-be/internal/entity" - "fmt" - "time" - - "go.uber.org/zap" - "gorm.io/gorm" -) - -type UndianRepo interface { - GetUndianEventByID(ctx mycontext.Context, id int64) (*entity.UndianEventDB, error) - GetActiveUndianEvents(ctx mycontext.Context) ([]*entity.UndianEventDB, error) - GetActiveUndianEventsWithPrizes(ctx mycontext.Context, customerID int64) ([]*entity.UndianEventDB, error) - GetCustomerVouchersByEventIDs(ctx mycontext.Context, customerID int64, eventIDs []int64) ([]*entity.UndianVoucherDB, error) - CreateUndianVouchers(ctx mycontext.Context, vouchers []*entity.UndianVoucherDB) error - GetNextVoucherSequence(ctx mycontext.Context) (int64, error) - GetNextVoucherSequenceBatch(ctx mycontext.Context, count int) (int64, error) -} - -type undianRepository struct { - db *gorm.DB -} - -func NewUndianRepository(db *gorm.DB) UndianRepo { - return &undianRepository{ - db: db, - } -} - -func (r *undianRepository) GetUndianEventByID(ctx mycontext.Context, id int64) (*entity.UndianEventDB, error) { - event := new(entity.UndianEventDB) - if err := r.db.WithContext(ctx).First(event, id).Error; err != nil { - logger.ContextLogger(ctx).Error("error when get undian event by id", zap.Error(err)) - return nil, err - } - return event, nil -} - -func (r *undianRepository) GetActiveUndianEvents(ctx mycontext.Context) ([]*entity.UndianEventDB, error) { - var events []*entity.UndianEventDB - now := time.Now() - - if err := r.db.WithContext(ctx). - Where("status = 'active' AND start_date <= ? AND end_date >= ?", now, now). - Order("created_at DESC"). - Find(&events).Error; err != nil { - logger.ContextLogger(ctx).Error("error when get active undian events", zap.Error(err)) - return nil, err - } - - return events, nil -} - -func (r *undianRepository) GetActiveUndianEventsWithPrizes(ctx mycontext.Context, customerID int64) ([]*entity.UndianEventDB, error) { - var events []*entity.UndianEventDB - now := time.Now() - - query := r.db.WithContext(ctx). - Preload("Prizes", func(db *gorm.DB) *gorm.DB { - return db.Order("rank ASC") - }). - Where("status = 'active' AND start_date <= ? AND end_date >= ?", now, now). - Order("created_at DESC") - - // If customer ID is provided, preload only that customer's vouchers - if customerID > 0 { - query = query.Preload("Vouchers", func(db *gorm.DB) *gorm.DB { - return db.Where("customer_id = ?", customerID).Order("created_at ASC") - }) - } else { - // If no customer ID, don't load any vouchers (or load all if needed) - query = query.Preload("Vouchers", func(db *gorm.DB) *gorm.DB { - return db.Where("1 = 0") // This loads no vouchers - }) - } - - if err := query.Find(&events).Error; err != nil { - logger.ContextLogger(ctx).Error("error when get active undian events with prizes", - zap.Int64("customerID", customerID), - zap.Error(err)) - return nil, err - } - - return events, nil -} - -func (r *undianRepository) GetCustomerVouchersByEventIDs(ctx mycontext.Context, customerID int64, eventIDs []int64) ([]*entity.UndianVoucherDB, error) { - var vouchers []*entity.UndianVoucherDB - - if err := r.db.WithContext(ctx). - Where("customer_id = ? AND undian_event_id IN ?", customerID, eventIDs). - Order("undian_event_id ASC, created_at ASC"). - Find(&vouchers).Error; err != nil { - logger.ContextLogger(ctx).Error("error when get customer vouchers by event IDs", - zap.Int64("customerID", customerID), - zap.Int64s("eventIDs", eventIDs), - zap.Error(err)) - return nil, err - } - - return vouchers, nil -} - -func (r *undianRepository) CreateUndianVouchers(ctx mycontext.Context, vouchers []*entity.UndianVoucherDB) error { - err := r.db.WithContext(ctx).Create(&vouchers).Error - if err != nil { - logger.ContextLogger(ctx).Error("error when create undian vouchers", zap.Error(err)) - return err - } - return nil -} - -func (r *undianRepository) GetNextVoucherSequence(ctx mycontext.Context) (int64, error) { - var sequence int64 - - err := r.db.WithContext(ctx).Raw("SELECT nextval('voucher_sequence')").Scan(&sequence).Error - if err != nil { - logger.ContextLogger(ctx).Error("error when get next voucher sequence", zap.Error(err)) - return 0, err - } - - return sequence, nil -} - -func (r *undianRepository) GetNextVoucherSequenceBatch(ctx mycontext.Context, count int) (int64, error) { - var startSequence int64 - - query := fmt.Sprintf("SELECT nextval('voucher_sequence') + %d - %d", count-1, count-1) - err := r.db.WithContext(ctx).Raw(query).Scan(&startSequence).Error - if err != nil { - logger.ContextLogger(ctx).Error("error when get batch voucher sequence", zap.Error(err)) - return 0, err - } - - return startSequence, nil -} diff --git a/internal/repository/user_repository.go b/internal/repository/user_repository.go new file mode 100644 index 0000000..a9e2747 --- /dev/null +++ b/internal/repository/user_repository.go @@ -0,0 +1,112 @@ +package repository + +import ( + "context" + + "apskel-pos-be/internal/entities" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type UserRepositoryImpl struct { + db *gorm.DB +} + +func NewUserRepository(db *gorm.DB) *UserRepositoryImpl { + return &UserRepositoryImpl{ + db: db, + } +} + +func (r *UserRepositoryImpl) Create(ctx context.Context, user *entities.User) error { + return r.db.WithContext(ctx).Create(user).Error +} + +func (r *UserRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID) (*entities.User, error) { + var user entities.User + err := r.db.WithContext(ctx).First(&user, "id = ?", id).Error + if err != nil { + return nil, err + } + return &user, nil +} + +func (r *UserRepositoryImpl) GetByEmail(ctx context.Context, email string) (*entities.User, error) { + var user entities.User + err := r.db.WithContext(ctx).Where("email = ?", email).First(&user).Error + if err != nil { + return nil, err + } + return &user, nil +} + +func (r *UserRepositoryImpl) GetByOrganizationID(ctx context.Context, organizationID uuid.UUID) ([]*entities.User, error) { + var users []*entities.User + err := r.db.WithContext(ctx).Where("organization_id = ?", organizationID).Find(&users).Error + return users, err +} + +func (r *UserRepositoryImpl) GetByRole(ctx context.Context, role entities.UserRole) ([]*entities.User, error) { + var users []*entities.User + err := r.db.WithContext(ctx).Where("role = ?", role).Find(&users).Error + return users, err +} + +func (r *UserRepositoryImpl) GetActiveUsers(ctx context.Context, organizationID uuid.UUID) ([]*entities.User, error) { + var users []*entities.User + err := r.db.WithContext(ctx). + Where("organization_id = ? AND is_active = ?", organizationID, true). + Find(&users).Error + return users, err +} + +func (r *UserRepositoryImpl) Update(ctx context.Context, user *entities.User) error { + return r.db.WithContext(ctx).Save(user).Error +} + +func (r *UserRepositoryImpl) Delete(ctx context.Context, id uuid.UUID) error { + return r.db.WithContext(ctx).Delete(&entities.User{}, "id = ?", id).Error +} + +func (r *UserRepositoryImpl) UpdatePassword(ctx context.Context, id uuid.UUID, passwordHash string) error { + return r.db.WithContext(ctx).Model(&entities.User{}). + Where("id = ?", id). + Update("password_hash", passwordHash).Error +} + +func (r *UserRepositoryImpl) UpdateActiveStatus(ctx context.Context, id uuid.UUID, isActive bool) error { + return r.db.WithContext(ctx).Model(&entities.User{}). + Where("id = ?", id). + Update("is_active", isActive).Error +} + +func (r *UserRepositoryImpl) List(ctx context.Context, filters map[string]interface{}, limit, offset int) ([]*entities.User, int64, error) { + var users []*entities.User + var total int64 + + query := r.db.WithContext(ctx).Model(&entities.User{}) + + for key, value := range filters { + query = query.Where(key+" = ?", value) + } + + if err := query.Count(&total).Error; err != nil { + return nil, 0, err + } + + err := query.Limit(limit).Offset(offset).Find(&users).Error + return users, total, err +} + +func (r *UserRepositoryImpl) Count(ctx context.Context, filters map[string]interface{}) (int64, error) { + var count int64 + query := r.db.WithContext(ctx).Model(&entities.User{}) + + for key, value := range filters { + query = query.Where(key+" = ?", value) + } + + err := query.Count(&count).Error + return count, err +} diff --git a/internal/repository/users/user.go b/internal/repository/users/user.go deleted file mode 100644 index c5669fa..0000000 --- a/internal/repository/users/user.go +++ /dev/null @@ -1,275 +0,0 @@ -package users - -import ( - "context" - "enaklo-pos-be/internal/common/logger" - "enaklo-pos-be/internal/entity" - - "go.uber.org/zap" - "gorm.io/gorm" -) - -type UserRepository struct { - db *gorm.DB -} - -func NewUserRepository(db *gorm.DB) *UserRepository { - return &UserRepository{ - db: db, - } -} - -func (r *UserRepository) Create(ctx context.Context, user *entity.UserDB) (*entity.UserDB, error) { - tx := r.db.Begin() - - user.ID = 0 - if err := tx.Select("name", "email", "password", "status", "created_by", "nik", "user_type", "phone_number").Create(user).Error; err != nil { - tx.Rollback() - logError(ctx, "creating user", err) - return nil, err - } - - if err := tx.First(user, user.ID).Error; err != nil { - tx.Rollback() - logError(ctx, "retrieving user", err) - return nil, err - } - - if user.UserType != "CUSTOMER" { - userRole := user.ToUserRoleDB() - if err := tx.Create(userRole).Error; err != nil { - tx.Rollback() - logError(ctx, "creating user role", err) - return nil, err - } - } - - if err := tx.Commit().Error; err != nil { - tx.Rollback() - logError(ctx, "committing transaction", err) - return nil, err - } - - return user, nil -} - -func (r *UserRepository) CreateWithTx(ctx context.Context, tx *gorm.DB, user *entity.UserDB) (*entity.UserDB, error) { - user.ID = 0 - if err := tx.Select("name", "email", "password", "status", "created_by", "nik", "user_type", "phone_number").Create(user).Error; err != nil { - logError(ctx, "creating user", err) - return nil, err - } - - if err := tx.First(user, user.ID).Error; err != nil { - tx.Rollback() - logError(ctx, "retrieving user", err) - return nil, err - } - - userRole := user.ToUserRoleDB() - if err := tx.Create(userRole).Error; err != nil { - tx.Rollback() - logError(ctx, "creating user role", err) - return nil, err - } - - return user, nil -} - -func (b *UserRepository) GetAllUsers(ctx context.Context, req entity.UserSearch) (entity.UserList, int, error) { - var users []*entity.UserDB - var total int64 - - query := b.db.Table("users"). - Select("users.id, users.email, users.name, users.status, users.created_at, users.updated_at, ur.role_id, r.role_name, ur.partner_id, b.name as partner_name"). - Joins("LEFT JOIN user_roles ur ON users.id = ur.user_id"). - Joins("LEFT JOIN roles r ON ur.role_id = r.role_id"). - Joins("LEFT JOIN partners b ON ur.partner_id = b.id"). - Where("users.deleted_at is null"). - Where("users.user_type != ?", "CUSTOMER") - - if req.Search != "" { - query = query.Where("users.name ILIKE ? or users.email ILIKE ? or r.role_name ILIKE ? or b.name ILIKE ? ", "%"+req.Search+"%", "%"+req.Search+"%", "%"+req.Search+"%", "%"+req.Search+"%") - } - - if req.Name != "" { - query = query.Where("users.name ILIKE ?", "%"+req.Name+"%") - } - - if req.RoleID > 0 { - query = query.Where("ur.role_id = ? ", req.RoleID) - } - - if req.PartnerID > 0 { - query = query.Where("ur.partner_id = ? ", req.PartnerID) - } - - if req.SiteID > 0 { - query = query.Where("ur.site_id = ? ", req.SiteID) - } - - // Get the total count without applying the limit and offset. - if err := query.Model(&entity.UserDB{}).Count(&total).Error; err != nil { - logger.ContextLogger(ctx).Error("error when count users", zap.Error(err)) - return nil, 0, err - } - - // Apply pagination. - query = query.Offset(req.Offset).Limit(req.Limit) - - if err := query.Scan(&users).Error; err != nil { - logger.ContextLogger(ctx).Error("error when get all users", zap.Error(err)) - return nil, 0, err - } - - return users, int(total), nil -} - -func (b *UserRepository) GetAllCustomer(ctx context.Context, req entity.CustomerSearch) (entity.CustomerList, int, error) { - var users []*entity.UserDB - var total int64 - - query := b.db.Table("customers"). - Select("customers.id, customers.email, customers.name, customers.phone as phone_number") - - if err := query.Model(&entity.UserDB{}).Count(&total).Error; err != nil { - logger.ContextLogger(ctx).Error("error when count users", zap.Error(err)) - return nil, 0, err - } - - // Apply pagination. - query = query.Offset(req.Offset).Limit(req.Limit) - - if err := query.Scan(&users).Error; err != nil { - logger.ContextLogger(ctx).Error("error when get all users", zap.Error(err)) - return nil, 0, err - } - - return users, int(total), nil -} - -func (b *UserRepository) GetUserByID(ctx context.Context, id int64) (*entity.UserDB, error) { - var user *entity.UserDB - - query := b.db.Table("users"). - Select("users.id, users.email,users.phone_number,users.nik, users.password , users.name, users.status, users.created_at, users.updated_at, ur.role_id, r.role_name, ur.partner_id, ur.site_id, b.name as partner_name"). - Joins("LEFT JOIN user_roles ur ON users.id = ur.user_id"). - Joins("LEFT JOIN roles r ON ur.role_id = r.role_id"). - Joins("LEFT JOIN partners b ON ur.partner_id = b.id"). - Where("users.id = ?", id) - - if err := query.Scan(&user).Error; err != nil { - logger.ContextLogger(ctx).Error("error when get user", zap.Error(err)) - return nil, err - } - - return user, nil -} - -func (b *UserRepository) GetPartnerAdmin(ctx context.Context, partnerID int64) (*entity.UserDB, error) { - var user *entity.UserDB - - partnerAdmin := 3 - - query := b.db.Table("users"). - Select("users.id, users.email,users.phone_number,users.nik, users.password , users.name, users.status, users.created_at, users.updated_at, ur.role_id, r.role_name, ur.partner_id, b.name as partner_name"). - Joins("LEFT JOIN user_roles ur ON users.id = ur.user_id"). - Joins("LEFT JOIN roles r ON ur.role_id = r.role_id"). - Joins("LEFT JOIN partners b ON ur.partner_id = b.id"). - Where("ur.partner_id = ?", partnerID). - Where("ur.role_id = ?", partnerAdmin) - - if err := query.Scan(&user).Error; err != nil { - logger.ContextLogger(ctx).Error("error when get user", zap.Error(err)) - return nil, err - } - - return user, nil -} - -func (b *UserRepository) GetUserByEmail(ctx context.Context, email string) (*entity.UserDB, error) { - var user *entity.UserDB - - query := b.db.Table("users"). - Select("users.id, users.email, users.name, users.status, users.created_at, users.updated_at, ur.role_id, r.role_name, ur.partner_id, b.name as partner_name"). - Joins("LEFT JOIN user_roles ur ON users.id = ur.user_id"). - Joins("LEFT JOIN roles r ON ur.role_id = r.role_id"). - Joins("LEFT JOIN partners b ON ur.partner_id = b.id"). - Where("users.email = ?", email) - - if err := query.Scan(&user).Error; err != nil { - logger.ContextLogger(ctx).Error("error when get user", zap.Error(err)) - return nil, err - } - - return user, nil -} - -func (r *UserRepository) UpdateUser(ctx context.Context, user *entity.UserDB) (*entity.UserDB, error) { - tx := r.db.Begin() - defer func() { - if r := recover(); r != nil { - tx.Rollback() - } - }() - - if err := tx.Select("name", "email", "password", "status", "deleted_at", "updated_by", "nik", "phone_number").Save(user).Error; err != nil { - tx.Rollback() - logError(ctx, "update user", err) - return nil, err - } - - userRole := user.ToUserRoleDB() - if err := tx.Model(userRole).Where("user_id = ?", user.ID).Updates(userRole).Error; err != nil { - tx.Rollback() - logError(ctx, "update user role", err) - return nil, err - } - - if err := tx.Commit().Error; err != nil { - tx.Rollback() - logError(ctx, "committing transaction", err) - return nil, err - } - - return user, nil -} - -func (r *UserRepository) UpdateUserWithTx(ctx context.Context, tx *gorm.DB, user *entity.UserDB) (*entity.UserDB, error) { - if err := tx.Select("name", "email", "nik", "phone_number", "password", "status", "deleted_at", "updated_by").Save(user).Error; err != nil { - logError(ctx, "update user", err) - return nil, err - } - - userRole := user.ToUserRoleDB() - if err := tx.Model(userRole).Where("user_id = ?", user.ID).Updates(userRole).Error; err != nil { - tx.Rollback() - logError(ctx, "update user role", err) - return nil, err - } - - return user, nil -} - -func logError(ctx context.Context, action string, err error) { - logger.ContextLogger(ctx).Error("error when "+action, zap.Error(err)) -} - -func (r *UserRepository) CountUsersByRoleAndSiteOrPartner(ctx context.Context, roleID int, siteID *int64) (int, error) { - var count int64 - - query := r.db.Table("users"). - Joins("LEFT JOIN user_roles ur ON users.id = ur.user_id"). - Where("ur.role_id = ?", roleID) - - if siteID != nil { - query = query.Where("ur.site_id = ?", siteID) - } - - if err := query.Count(&count).Error; err != nil { - logger.ContextLogger(ctx).Error("error when counting users by role and site/partner", zap.Error(err)) - return 0, err - } - - return int(count), nil -} diff --git a/internal/repository/wallet/wallet.go b/internal/repository/wallet/wallet.go deleted file mode 100644 index e20af4a..0000000 --- a/internal/repository/wallet/wallet.go +++ /dev/null @@ -1,94 +0,0 @@ -package repository - -import ( - "context" - "enaklo-pos-be/internal/common/logger" - "enaklo-pos-be/internal/entity" - "go.uber.org/zap" - "gorm.io/gorm" - "gorm.io/gorm/clause" -) - -type WalletRepository struct { - db *gorm.DB -} - -func NewWalletRepository(db *gorm.DB) *WalletRepository { - return &WalletRepository{ - db: db, - } -} - -func (r *WalletRepository) Create(ctx context.Context, tx *gorm.DB, wallet *entity.Wallet) (*entity.Wallet, error) { - err := tx.Create(wallet).Error - if err != nil { - logger.ContextLogger(ctx).Error("error when creating wallet", zap.Error(err)) - return nil, err - } - return wallet, nil -} - -func (r *WalletRepository) Update(ctx context.Context, db *gorm.DB, wallet *entity.Wallet) (*entity.Wallet, error) { - if err := db.Save(wallet).Error; err != nil { - logger.ContextLogger(ctx).Error("error when updating wallet", zap.Error(err)) - return nil, err - } - return wallet, nil -} - -func (r *WalletRepository) GetByPartnerID(ctx context.Context, db *gorm.DB, partnerID int64) (*entity.Wallet, error) { - if db == nil { - db = r.db - } - - wallet := new(entity.Wallet) - if err := db.WithContext(ctx).Where("partner_id = ?", partnerID).First(wallet).Error; err != nil { - logger.ContextLogger(ctx).Error("error when finding wallet by partner ID", zap.Error(err)) - return nil, err - } - return wallet, nil -} - -func (r *WalletRepository) GetByID(ctx context.Context, id int64) (*entity.Wallet, error) { - wallet := new(entity.Wallet) - if err := r.db.First(wallet, id).Error; err != nil { - logger.ContextLogger(ctx).Error("error when getting wallet by id", zap.Error(err)) - return nil, err - } - return wallet, nil -} - -func (r *WalletRepository) GetForUpdate(ctx context.Context, tx *gorm.DB, partnerID int64) (*entity.Wallet, error) { - if tx == nil { - tx = r.db - } - - query := tx.WithContext(ctx).Where("partner_id = ?", partnerID). - Clauses(clause.Locking{Strength: "UPDATE"}) - - wallet := new(entity.Wallet) - if err := query.First(wallet).Error; err != nil { - logger.ContextLogger(ctx).Error("error when finding balance by partner ID", zap.Error(err)) - return nil, err - } - return wallet, nil -} - -func (r *WalletRepository) GetAll(ctx context.Context) ([]*entity.Wallet, error) { - var wallets []*entity.Wallet - if err := r.db.Find(&wallets).Error; err != nil { - logger.ContextLogger(ctx).Error("error when getting all wallets", zap.Error(err)) - return nil, err - } - return wallets, nil -} - -func (r *WalletRepository) Delete(ctx context.Context, id int64) error { - wallet := new(entity.Wallet) - wallet.ID = id - if err := r.db.Delete(wallet).Error; err != nil { - logger.ContextLogger(ctx).Error("error when deleting wallet", zap.Error(err)) - return err - } - return nil -} diff --git a/internal/router/router.go b/internal/router/router.go new file mode 100644 index 0000000..af4e625 --- /dev/null +++ b/internal/router/router.go @@ -0,0 +1,262 @@ +package router + +import ( + "apskel-pos-be/config" + "apskel-pos-be/internal/handler" + "apskel-pos-be/internal/middleware" + "apskel-pos-be/internal/service" + "apskel-pos-be/internal/transformer" + "apskel-pos-be/internal/validator" + + "github.com/gin-gonic/gin" +) + +type Router struct { + config *config.Config + healthHandler *handler.HealthHandler + authHandler *handler.AuthHandler + userHandler *handler.UserHandler + organizationHandler *handler.OrganizationHandler + outletHandler *handler.OutletHandler + outletSettingHandler *handler.OutletSettingHandlerImpl + categoryHandler *handler.CategoryHandler + productHandler *handler.ProductHandler + productVariantHandler *handler.ProductVariantHandler + inventoryHandler *handler.InventoryHandler + orderHandler *handler.OrderHandler + fileHandler *handler.FileHandler + customerHandler *handler.CustomerHandler + paymentMethodHandler *handler.PaymentMethodHandler + analyticsHandler *handler.AnalyticsHandler + authMiddleware *middleware.AuthMiddleware +} + +func NewRouter(cfg *config.Config, + healthHandler *handler.HealthHandler, + authService service.AuthService, + authMiddleware *middleware.AuthMiddleware, + userService *service.UserServiceImpl, + userValidator *validator.UserValidatorImpl, + organizationService service.OrganizationService, + organizationValidator validator.OrganizationValidator, + outletService service.OutletService, + outletValidator validator.OutletValidator, + outletSettingService service.OutletSettingService, + categoryService service.CategoryService, + categoryValidator validator.CategoryValidator, + productService service.ProductService, + productValidator validator.ProductValidator, + productVariantService service.ProductVariantService, + productVariantValidator validator.ProductVariantValidator, + inventoryService service.InventoryService, + inventoryValidator validator.InventoryValidator, + orderService service.OrderService, + orderValidator validator.OrderValidator, + fileService service.FileService, + fileValidator validator.FileValidator, + customerService service.CustomerService, + customerValidator validator.CustomerValidator, + paymentMethodService service.PaymentMethodService, + paymentMethodValidator validator.PaymentMethodValidator, + analyticsService *service.AnalyticsServiceImpl) *Router { + + return &Router{ + config: cfg, + healthHandler: healthHandler, + authHandler: handler.NewAuthHandler(authService), + userHandler: handler.NewUserHandler(userService, userValidator), + organizationHandler: handler.NewOrganizationHandler(organizationService, organizationValidator), + outletHandler: handler.NewOutletHandler(outletService, outletValidator), + outletSettingHandler: handler.NewOutletSettingHandlerImpl(outletSettingService), + categoryHandler: handler.NewCategoryHandler(categoryService, categoryValidator), + productHandler: handler.NewProductHandler(productService, productValidator), + inventoryHandler: handler.NewInventoryHandler(inventoryService, inventoryValidator), + orderHandler: handler.NewOrderHandler(orderService, orderValidator, transformer.NewTransformer()), + fileHandler: handler.NewFileHandler(fileService, fileValidator, transformer.NewTransformer()), + customerHandler: handler.NewCustomerHandler(customerService, customerValidator), + paymentMethodHandler: handler.NewPaymentMethodHandler(paymentMethodService, paymentMethodValidator), + analyticsHandler: handler.NewAnalyticsHandler(analyticsService, transformer.NewTransformer()), + authMiddleware: authMiddleware, + } +} + +func (r *Router) Init() *gin.Engine { + gin.SetMode(gin.ReleaseMode) + engine := gin.New() + engine.Use( + middleware.JsonAPI(), + middleware.CorrelationID(), + middleware.Recover(), + middleware.HTTPStatLogger(), + middleware.PopulateContext(), + ) + + r.addAppRoutes(engine) + return engine +} + +func (r *Router) addAppRoutes(rg *gin.Engine) { + rg.GET("/health", r.healthHandler.HealthCheck) + + v1 := rg.Group("/api/v1") + { + auth := v1.Group("/auth") + { + auth.POST("/login", r.authHandler.Login) + auth.POST("/logout", r.authHandler.Logout) + auth.POST("/refresh", r.authHandler.RefreshToken) + auth.GET("/validate", r.authHandler.ValidateToken) + auth.GET("/profile", r.authHandler.GetProfile) + } + + organizations := v1.Group("/organizations") + { + organizations.POST("", r.organizationHandler.CreateOrganization) + } + + protected := v1.Group("") + protected.Use(r.authMiddleware.RequireAuth()) + { + users := protected.Group("/users") + { + adminUsers := users.Group("") + adminUsers.Use(r.authMiddleware.RequireAdminOrManager()) + { + adminUsers.POST("", r.userHandler.CreateUser) + adminUsers.GET("", r.userHandler.ListUsers) + adminUsers.GET("/:id", r.userHandler.GetUser) + adminUsers.PUT("/:id", r.userHandler.UpdateUser) + adminUsers.DELETE("/:id", r.userHandler.DeleteUser) + adminUsers.PUT("/:id/activate", r.userHandler.ActivateUser) + adminUsers.PUT("/:id/deactivate", r.userHandler.DeactivateUser) + } + + users.PUT("/:id/password", r.userHandler.ChangePassword) + } + + protectedOrganizations := protected.Group("/organizations") + { + adminOrgRoutes := protectedOrganizations.Group("") + adminOrgRoutes.Use(r.authMiddleware.RequireSuperAdmin()) + { + adminOrgRoutes.GET("", r.organizationHandler.ListOrganizations) + adminOrgRoutes.GET("/:id", r.organizationHandler.GetOrganization) + adminOrgRoutes.PUT("/:id", r.organizationHandler.UpdateOrganization) + adminOrgRoutes.DELETE("/:id", r.organizationHandler.DeleteOrganization) + } + } + + categories := protected.Group("/categories") + categories.Use(r.authMiddleware.RequireAdminOrManager()) + { + categories.POST("", r.categoryHandler.CreateCategory) + categories.GET("", r.categoryHandler.ListCategories) + categories.GET("/:id", r.categoryHandler.GetCategory) + categories.PUT("/:id", r.categoryHandler.UpdateCategory) + categories.DELETE("/:id", r.categoryHandler.DeleteCategory) + } + + products := protected.Group("/products") + products.Use(r.authMiddleware.RequireAdminOrManager()) + { + products.POST("", r.productHandler.CreateProduct) + products.GET("", r.productHandler.ListProducts) + products.GET("/:id", r.productHandler.GetProduct) + products.PUT("/:id", r.productHandler.UpdateProduct) + products.DELETE("/:id", r.productHandler.DeleteProduct) + } + + inventory := protected.Group("/inventory") + inventory.Use(r.authMiddleware.RequireAdminOrManager()) + { + inventory.POST("", r.inventoryHandler.CreateInventory) + inventory.GET("", r.inventoryHandler.ListInventory) + inventory.GET("/:id", r.inventoryHandler.GetInventory) + inventory.PUT("/:id", r.inventoryHandler.UpdateInventory) + inventory.DELETE("/:id", r.inventoryHandler.DeleteInventory) + inventory.POST("/adjust", r.inventoryHandler.AdjustInventory) + inventory.GET("/low-stock/:outlet_id", r.inventoryHandler.GetLowStockItems) + inventory.GET("/zero-stock/:outlet_id", r.inventoryHandler.GetZeroStockItems) + } + + orders := protected.Group("/orders") + orders.Use(r.authMiddleware.RequireAdminOrManager()) + { + orders.GET("", r.orderHandler.ListOrders) + orders.GET("/:id", r.orderHandler.GetOrderByID) + orders.POST("", r.orderHandler.CreateOrder) + orders.POST("/:id/add-items", r.orderHandler.AddToOrder) + orders.PUT("/:id", r.orderHandler.UpdateOrder) + orders.PUT("/:id/customer", r.orderHandler.SetOrderCustomer) + orders.POST("/void", r.orderHandler.VoidOrder) + orders.POST("/:id/refund", r.orderHandler.RefundOrder) + } + + payments := protected.Group("/payments") + payments.Use(r.authMiddleware.RequireAdminOrManager()) + { + payments.POST("", r.orderHandler.CreatePayment) + payments.POST("/:id/refund", r.orderHandler.RefundPayment) + } + + paymentMethods := protected.Group("/payment-methods") + paymentMethods.Use(r.authMiddleware.RequireAdminOrManager()) + { + paymentMethods.POST("", r.paymentMethodHandler.CreatePaymentMethod) + paymentMethods.GET("", r.paymentMethodHandler.ListPaymentMethods) + paymentMethods.GET("/:id", r.paymentMethodHandler.GetPaymentMethod) + paymentMethods.PUT("/:id", r.paymentMethodHandler.UpdatePaymentMethod) + paymentMethods.DELETE("/:id", r.paymentMethodHandler.DeletePaymentMethod) + paymentMethods.GET("/organization/:organization_id/active", r.paymentMethodHandler.GetActivePaymentMethodsByOrganization) + } + + files := protected.Group("/files") + files.Use(r.authMiddleware.RequireAdminOrManager()) + { + files.GET("/organization", r.fileHandler.GetFilesByOrganization) + files.GET("/user", r.fileHandler.GetFilesByUser) + files.GET("/:id", r.fileHandler.GetFileByID) + files.POST("/upload", r.fileHandler.UploadFile) + files.PUT("/:id", r.fileHandler.UpdateFile) + } + + outlets := protected.Group("/outlets") + outlets.Use(r.authMiddleware.RequireAdminOrManager()) + { + outlets.GET("/list", r.outletHandler.ListOutlets) + outlets.GET("/:id", r.outletHandler.GetOutlet) + outlets.PUT("/:id", r.outletHandler.UpdateOutlet) + outlets.GET("/printer-setting/:outlet_id", r.outletSettingHandler.GetPrinterSettings) + outlets.PUT("/printer-setting/:outlet_id", r.outletSettingHandler.UpdatePrinterSettings) + } + + customers := protected.Group("/customers") + customers.Use(r.authMiddleware.RequireAdminOrManager()) + { + customers.POST("", r.customerHandler.CreateCustomer) + customers.GET("", r.customerHandler.ListCustomers) + customers.GET("/:id", r.customerHandler.GetCustomer) + customers.PUT("/:id", r.customerHandler.UpdateCustomer) + customers.DELETE("/:id", r.customerHandler.DeleteCustomer) + customers.POST("/set-default", r.customerHandler.SetDefaultCustomer) + customers.GET("/default", r.customerHandler.GetDefaultCustomer) + } + + analytics := protected.Group("/analytics") + analytics.Use(r.authMiddleware.RequireAdminOrManager()) + { + analytics.GET("/payment-methods", r.analyticsHandler.GetPaymentMethodAnalytics) + analytics.GET("/sales", r.analyticsHandler.GetSalesAnalytics) + analytics.GET("/products", r.analyticsHandler.GetProductAnalytics) + analytics.GET("/dashboard", r.analyticsHandler.GetDashboardAnalytics) + analytics.GET("/profit-loss", r.analyticsHandler.GetProfitLossAnalytics) + } + + //outletPrinterSettings := protected.Group("/outlets/:outlet_id/settings") + //outletPrinterSettings.Use(r.authMiddleware.RequireAdminOrManager()) + //{ + // + //} + } + } +} diff --git a/internal/routes/customer_routes.go b/internal/routes/customer_routes.go deleted file mode 100644 index 6abcfc6..0000000 --- a/internal/routes/customer_routes.go +++ /dev/null @@ -1,31 +0,0 @@ -package routes - -import ( - "enaklo-pos-be/internal/handlers/http" - "enaklo-pos-be/internal/handlers/http/customerauth" - "enaklo-pos-be/internal/middlewares" - - "enaklo-pos-be/internal/app" - "enaklo-pos-be/internal/repository" - "enaklo-pos-be/internal/services" -) - -func RegisterCustomerRoutes(app *app.Server, serviceManager *services.ServiceManagerImpl, - repoManager *repository.RepoManagerImpl) { - approute := app.Group("/api/v1/customer") - - authMiddleware := middlewares.CustomerAuthorizationMiddleware(repoManager.Crypto) - optionlMiddleWare := middlewares.OptionalCustomerAuthorizationMiddleware(repoManager.Crypto) - - serverRoutes := []HTTPHandlerRoutes{ - customerauth.NewAuthHandler(serviceManager.AuthV2Svc, serviceManager.MemberRegistrationSvc), - http.NewMenuHandler(serviceManager.ProductV2Svc, serviceManager.InProgressSvc), - http.NewCustomerOrderHandler(serviceManager.OrderV2Svc), - } - - for _, handler := range serverRoutes { - handler.Route(approute, authMiddleware) - } - - http.NewCustomerUndianHandler(serviceManager.UndianSvc).Route(approute, optionlMiddleWare) -} diff --git a/internal/routes/routes.go b/internal/routes/routes.go deleted file mode 100644 index 773192a..0000000 --- a/internal/routes/routes.go +++ /dev/null @@ -1,81 +0,0 @@ -package routes - -import ( - http2 "enaklo-pos-be/internal/handlers/http" - "enaklo-pos-be/internal/handlers/http/balance" - "enaklo-pos-be/internal/handlers/http/license" - "enaklo-pos-be/internal/handlers/http/oss" - "enaklo-pos-be/internal/handlers/http/partner" - "enaklo-pos-be/internal/handlers/http/product" - site "enaklo-pos-be/internal/handlers/http/sites" - "enaklo-pos-be/internal/handlers/http/transaction" - "enaklo-pos-be/internal/handlers/http/user" - "net/http" - - swaggerFiles "github.com/swaggo/files" - ginSwagger "github.com/swaggo/gin-swagger" - - "enaklo-pos-be/internal/middlewares" - - "github.com/gin-gonic/gin" - - "enaklo-pos-be/internal/app" - "enaklo-pos-be/internal/handlers/http/auth" - "enaklo-pos-be/internal/repository" - "enaklo-pos-be/internal/services" -) - -func RegisterPublicRoutes(app *app.Server, serviceManager *services.ServiceManagerImpl, - repoManager *repository.RepoManagerImpl) { - route := app.Group("/") - route.GET("/", func(c *gin.Context) { - c.JSON(http.StatusOK, "HEALTHY") - }) - route.GET("/docs/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) -} - -type HTTPHandlerRoutes interface { - Route(group *gin.RouterGroup, jwt gin.HandlerFunc) -} - -func RegisterPrivateRoutes(app *app.Server, serviceManager *services.ServiceManagerImpl, - repoManager *repository.RepoManagerImpl) { - approute := app.Group("/api/v1") - - authMiddleware := middlewares.AuthorizationMiddleware(repoManager.Crypto) - serverRoutes := []HTTPHandlerRoutes{ - auth.NewAuthHandler(serviceManager.AuthSvc), - user.NewHandler(serviceManager.UserSvc), - product.NewHandler(serviceManager.ProductSvc), - oss.NewOssHandler(serviceManager.OSSSvc), - partner.NewHandler(serviceManager.PartnerSvc), - site.NewHandler(serviceManager.SiteSvc), - license.NewHandler(serviceManager.LicenseSvc), - transaction.New(serviceManager.Transaction), - balance.NewHandler(serviceManager.Balance), - } - - for _, handler := range serverRoutes { - handler.Route(approute, authMiddleware) - } -} - -func RegisterPrivateRoutesV2(app *app.Server, serviceManager *services.ServiceManagerImpl, - repoManager *repository.RepoManagerImpl) { - approute := app.Group("/api/v2") - - authMiddleware := middlewares.AuthorizationMiddleware(repoManager.Crypto) - - serverRoutes := []HTTPHandlerRoutes{ - http2.NewOrderHandler(serviceManager.OrderV2Svc), - http2.NewMemberRegistrationHandler(serviceManager.MemberRegistrationSvc), - http2.NewCustomerHandler(serviceManager.CustomerV2Svc), - http2.NewInProgressOrderHandler(serviceManager.InProgressSvc), - http2.NewCashierSession(serviceManager.CashierSvc), - http2.NewCategoryHandler(serviceManager.CategorySvc), - } - - for _, handler := range serverRoutes { - handler.Route(approute, authMiddleware) - } -} diff --git a/internal/service/analytics_service.go b/internal/service/analytics_service.go new file mode 100644 index 0000000..65e955b --- /dev/null +++ b/internal/service/analytics_service.go @@ -0,0 +1,243 @@ +package service + +import ( + "context" + "fmt" + + "apskel-pos-be/internal/models" + "apskel-pos-be/internal/processor" + + "github.com/google/uuid" +) + +type AnalyticsService interface { + GetPaymentMethodAnalytics(ctx context.Context, req *models.PaymentMethodAnalyticsRequest) (*models.PaymentMethodAnalyticsResponse, error) + GetSalesAnalytics(ctx context.Context, req *models.SalesAnalyticsRequest) (*models.SalesAnalyticsResponse, error) + GetProductAnalytics(ctx context.Context, req *models.ProductAnalyticsRequest) (*models.ProductAnalyticsResponse, error) + GetDashboardAnalytics(ctx context.Context, req *models.DashboardAnalyticsRequest) (*models.DashboardAnalyticsResponse, error) + GetProfitLossAnalytics(ctx context.Context, req *models.ProfitLossAnalyticsRequest) (*models.ProfitLossAnalyticsResponse, error) +} + +type AnalyticsServiceImpl struct { + analyticsProcessor processor.AnalyticsProcessor +} + +func NewAnalyticsServiceImpl(analyticsProcessor processor.AnalyticsProcessor) *AnalyticsServiceImpl { + return &AnalyticsServiceImpl{ + analyticsProcessor: analyticsProcessor, + } +} + +func (s *AnalyticsServiceImpl) GetPaymentMethodAnalytics(ctx context.Context, req *models.PaymentMethodAnalyticsRequest) (*models.PaymentMethodAnalyticsResponse, error) { + // Validate request + if err := s.validatePaymentMethodAnalyticsRequest(req); err != nil { + return nil, fmt.Errorf("validation error: %w", err) + } + + // Process analytics request + response, err := s.analyticsProcessor.GetPaymentMethodAnalytics(ctx, req) + if err != nil { + return nil, fmt.Errorf("failed to get payment method analytics: %w", err) + } + + return response, nil +} + +func (s *AnalyticsServiceImpl) GetSalesAnalytics(ctx context.Context, req *models.SalesAnalyticsRequest) (*models.SalesAnalyticsResponse, error) { + // Validate request + if err := s.validateSalesAnalyticsRequest(req); err != nil { + return nil, fmt.Errorf("validation error: %w", err) + } + + // Process analytics request + response, err := s.analyticsProcessor.GetSalesAnalytics(ctx, req) + if err != nil { + return nil, fmt.Errorf("failed to get sales analytics: %w", err) + } + + return response, nil +} + +func (s *AnalyticsServiceImpl) GetProductAnalytics(ctx context.Context, req *models.ProductAnalyticsRequest) (*models.ProductAnalyticsResponse, error) { + // Validate request + if err := s.validateProductAnalyticsRequest(req); err != nil { + return nil, fmt.Errorf("validation error: %w", err) + } + + // Process analytics request + response, err := s.analyticsProcessor.GetProductAnalytics(ctx, req) + if err != nil { + return nil, fmt.Errorf("failed to get product analytics: %w", err) + } + + return response, nil +} + +func (s *AnalyticsServiceImpl) GetDashboardAnalytics(ctx context.Context, req *models.DashboardAnalyticsRequest) (*models.DashboardAnalyticsResponse, error) { + // Validate request + if err := s.validateDashboardAnalyticsRequest(req); err != nil { + return nil, fmt.Errorf("validation error: %w", err) + } + + // Process analytics request + response, err := s.analyticsProcessor.GetDashboardAnalytics(ctx, req) + if err != nil { + return nil, fmt.Errorf("failed to get dashboard analytics: %w", err) + } + + return response, nil +} + +// Validation methods +func (s *AnalyticsServiceImpl) validatePaymentMethodAnalyticsRequest(req *models.PaymentMethodAnalyticsRequest) error { + if req.OrganizationID == uuid.Nil { + return fmt.Errorf("organization ID is required") + } + + if req.DateFrom.IsZero() { + return fmt.Errorf("date_from is required") + } + + if req.DateTo.IsZero() { + return fmt.Errorf("date_to is required") + } + + if req.DateFrom.After(req.DateTo) { + return fmt.Errorf("date_from cannot be after date_to") + } + + // Validate groupBy if provided + if req.GroupBy != "" { + validGroupBy := map[string]bool{ + "day": true, + "hour": true, + "week": true, + "month": true, + } + if !validGroupBy[req.GroupBy] { + return fmt.Errorf("invalid group_by value: %s", req.GroupBy) + } + } + + return nil +} + +func (s *AnalyticsServiceImpl) validateSalesAnalyticsRequest(req *models.SalesAnalyticsRequest) error { + if req.OrganizationID == uuid.Nil { + return fmt.Errorf("organization ID is required") + } + + if req.DateFrom.IsZero() { + return fmt.Errorf("date_from is required") + } + + if req.DateTo.IsZero() { + return fmt.Errorf("date_to is required") + } + + if req.DateFrom.After(req.DateTo) { + return fmt.Errorf("date_from cannot be after date_to") + } + + // Validate groupBy if provided + if req.GroupBy != "" { + validGroupBy := map[string]bool{ + "day": true, + "hour": true, + "week": true, + "month": true, + } + if !validGroupBy[req.GroupBy] { + return fmt.Errorf("invalid group_by value: %s", req.GroupBy) + } + } + + return nil +} + +func (s *AnalyticsServiceImpl) validateProductAnalyticsRequest(req *models.ProductAnalyticsRequest) error { + if req.OrganizationID == uuid.Nil { + return fmt.Errorf("organization ID is required") + } + + if req.DateFrom.IsZero() { + return fmt.Errorf("date_from is required") + } + + if req.DateTo.IsZero() { + return fmt.Errorf("date_to is required") + } + + if req.DateFrom.After(req.DateTo) { + return fmt.Errorf("date_from cannot be after date_to") + } + + if req.Limit < 1 || req.Limit > 100 { + return fmt.Errorf("limit must be between 1 and 100") + } + + return nil +} + +func (s *AnalyticsServiceImpl) validateDashboardAnalyticsRequest(req *models.DashboardAnalyticsRequest) error { + if req.OrganizationID == uuid.Nil { + return fmt.Errorf("organization ID is required") + } + + if req.DateFrom.IsZero() { + return fmt.Errorf("date_from is required") + } + + if req.DateTo.IsZero() { + return fmt.Errorf("date_to is required") + } + + if req.DateFrom.After(req.DateTo) { + return fmt.Errorf("date_from cannot be after date_to") + } + + return nil +} + +func (s *AnalyticsServiceImpl) GetProfitLossAnalytics(ctx context.Context, req *models.ProfitLossAnalyticsRequest) (*models.ProfitLossAnalyticsResponse, error) { + // Validate request + if err := s.validateProfitLossAnalyticsRequest(req); err != nil { + return nil, fmt.Errorf("validation error: %w", err) + } + + // Call processor + response, err := s.analyticsProcessor.GetProfitLossAnalytics(ctx, req) + if err != nil { + return nil, fmt.Errorf("failed to get profit/loss analytics: %w", err) + } + + return response, nil +} + +func (s *AnalyticsServiceImpl) validateProfitLossAnalyticsRequest(req *models.ProfitLossAnalyticsRequest) error { + if req == nil { + return fmt.Errorf("request cannot be nil") + } + + if req.OrganizationID == uuid.Nil { + return fmt.Errorf("organization_id is required") + } + + if req.DateFrom.IsZero() { + return fmt.Errorf("date_from is required") + } + + if req.DateTo.IsZero() { + return fmt.Errorf("date_to is required") + } + + if req.DateFrom.After(req.DateTo) { + return fmt.Errorf("date_from cannot be after date_to") + } + + if req.GroupBy != "" && req.GroupBy != "hour" && req.GroupBy != "day" && req.GroupBy != "week" && req.GroupBy != "month" { + return fmt.Errorf("invalid group_by value, must be one of: hour, day, week, month") + } + + return nil +} diff --git a/internal/service/auth_service.go b/internal/service/auth_service.go new file mode 100644 index 0000000..d00d51a --- /dev/null +++ b/internal/service/auth_service.go @@ -0,0 +1,184 @@ +package service + +import ( + "context" + "errors" + "fmt" + "time" + + "apskel-pos-be/internal/contract" + "apskel-pos-be/internal/models" + "apskel-pos-be/internal/transformer" + + "github.com/golang-jwt/jwt/v5" + "github.com/google/uuid" + "golang.org/x/crypto/bcrypt" +) + +type AuthService interface { + Login(ctx context.Context, req *contract.LoginRequest) (*contract.LoginResponse, error) + ValidateToken(tokenString string) (*contract.UserResponse, error) + RefreshToken(ctx context.Context, tokenString string) (*contract.LoginResponse, error) + Logout(ctx context.Context, tokenString string) error +} + +type AuthServiceImpl struct { + userProcessor UserProcessor + jwtSecret string + tokenTTL time.Duration +} + +type Claims struct { + UserID uuid.UUID `json:"user_id"` + Email string `json:"email"` + Role string `json:"role"` + OrganizationID uuid.UUID `json:"organization_id"` + jwt.RegisteredClaims +} + +func NewAuthService(userProcessor UserProcessor, jwtSecret string) AuthService { + return &AuthServiceImpl{ + userProcessor: userProcessor, + jwtSecret: jwtSecret, + tokenTTL: 24 * time.Hour, + } +} + +func (s *AuthServiceImpl) Login(ctx context.Context, req *contract.LoginRequest) (*contract.LoginResponse, error) { + userResponse, err := s.userProcessor.GetUserByEmail(ctx, req.Email) + if err != nil { + return nil, fmt.Errorf("invalid credentials") + } + + if !userResponse.IsActive { + return nil, fmt.Errorf("user account is deactivated") + } + + userEntity, err := s.userProcessor.GetUserEntityByEmail(ctx, req.Email) + if err != nil { + return nil, fmt.Errorf("invalid credentials") + } + + err = bcrypt.CompareHashAndPassword([]byte(userEntity.PasswordHash), []byte(req.Password)) + if err != nil { + return nil, fmt.Errorf("invalid credentials") + } + + contractUserResponse := transformer.UserModelResponseToResponse(userResponse) + + token, expiresAt, err := s.generateToken(userResponse) + if err != nil { + return nil, fmt.Errorf("failed to generate token: %w", err) + } + + return &contract.LoginResponse{ + Token: token, + ExpiresAt: expiresAt, + User: *contractUserResponse, + }, nil +} + +func (s *AuthServiceImpl) ValidateToken(tokenString string) (*contract.UserResponse, error) { + claims, err := s.parseToken(tokenString) + if err != nil { + return nil, fmt.Errorf("invalid token: %w", err) + } + + userResponse, err := s.userProcessor.GetUserByID(context.Background(), claims.UserID) + if err != nil { + return nil, fmt.Errorf("user not found: %w", err) + } + + if !userResponse.IsActive { + return nil, fmt.Errorf("user account is deactivated") + } + + contractUserResponse := transformer.UserModelResponseToResponse(userResponse) + return contractUserResponse, nil +} + +func (s *AuthServiceImpl) RefreshToken(ctx context.Context, tokenString string) (*contract.LoginResponse, error) { + claims, err := s.parseToken(tokenString) + if err != nil { + return nil, fmt.Errorf("invalid token: %w", err) + } + + userResponse, err := s.userProcessor.GetUserByID(ctx, claims.UserID) + if err != nil { + return nil, fmt.Errorf("user not found: %w", err) + } + + if !userResponse.IsActive { + return nil, fmt.Errorf("user account is deactivated") + } + + contractUserResponse := transformer.UserModelResponseToResponse(userResponse) + + newToken, expiresAt, err := s.generateToken(userResponse) + if err != nil { + return nil, fmt.Errorf("failed to generate token: %w", err) + } + + return &contract.LoginResponse{ + Token: newToken, + ExpiresAt: expiresAt, + User: *contractUserResponse, + }, nil +} + +func (s *AuthServiceImpl) Logout(ctx context.Context, tokenString string) error { + // In a more sophisticated implementation, you might want to blacklist the token + // For now, we'll just validate that the token is valid + _, err := s.parseToken(tokenString) + if err != nil { + return fmt.Errorf("invalid token: %w", err) + } + + // In the future, you could store blacklisted tokens in Redis or database + return nil +} + +func (s *AuthServiceImpl) generateToken(user *models.UserResponse) (string, time.Time, error) { + expiresAt := time.Now().Add(s.tokenTTL) + + claims := &Claims{ + UserID: user.ID, + Email: user.Email, + Role: string(user.Role), + OrganizationID: user.OrganizationID, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(expiresAt), + IssuedAt: jwt.NewNumericDate(time.Now()), + NotBefore: jwt.NewNumericDate(time.Now()), + Issuer: "apskel-pos", + Subject: user.ID.String(), + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + tokenString, err := token.SignedString([]byte(s.jwtSecret)) + if err != nil { + return "", time.Time{}, err + } + + return tokenString, expiresAt, nil +} + +func (s *AuthServiceImpl) parseToken(tokenString string) (*Claims, error) { + token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + return []byte(s.jwtSecret), nil + }) + + if err != nil { + return nil, err + } + + if claims, ok := token.Claims.(*Claims); ok && token.Valid { + return claims, nil + } + + return nil, errors.New("invalid token") +} diff --git a/internal/service/category_service.go b/internal/service/category_service.go new file mode 100644 index 0000000..cfec8aa --- /dev/null +++ b/internal/service/category_service.go @@ -0,0 +1,119 @@ +package service + +import ( + "context" + + "apskel-pos-be/internal/appcontext" + "apskel-pos-be/internal/constants" + "apskel-pos-be/internal/contract" + "apskel-pos-be/internal/processor" + "apskel-pos-be/internal/transformer" + + "github.com/google/uuid" +) + +type CategoryService interface { + CreateCategory(ctx context.Context, apctx *appcontext.ContextInfo, req *contract.CreateCategoryRequest) *contract.Response + UpdateCategory(ctx context.Context, id uuid.UUID, req *contract.UpdateCategoryRequest) *contract.Response + DeleteCategory(ctx context.Context, id uuid.UUID) *contract.Response + GetCategoryByID(ctx context.Context, id uuid.UUID) *contract.Response + ListCategories(ctx context.Context, req *contract.ListCategoriesRequest) *contract.Response +} + +type CategoryServiceImpl struct { + categoryProcessor processor.CategoryProcessor +} + +func NewCategoryService(categoryProcessor processor.CategoryProcessor) *CategoryServiceImpl { + return &CategoryServiceImpl{ + categoryProcessor: categoryProcessor, + } +} + +func (s *CategoryServiceImpl) CreateCategory(ctx context.Context, apctx *appcontext.ContextInfo, req *contract.CreateCategoryRequest) *contract.Response { + modelReq := transformer.CreateCategoryRequestToModel(apctx, req) + + categoryResponse, err := s.categoryProcessor.CreateCategory(ctx, modelReq) + if err != nil { + errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.CategoryServiceEntity, err.Error()) + return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) + } + + contractResponse := transformer.CategoryModelResponseToResponse(categoryResponse) + return contract.BuildSuccessResponse(contractResponse) +} + +func (s *CategoryServiceImpl) UpdateCategory(ctx context.Context, id uuid.UUID, req *contract.UpdateCategoryRequest) *contract.Response { + modelReq := transformer.UpdateCategoryRequestToModel(req) + + categoryResponse, err := s.categoryProcessor.UpdateCategory(ctx, id, modelReq) + if err != nil { + errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.CategoryServiceEntity, err.Error()) + return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) + } + + contractResponse := transformer.CategoryModelResponseToResponse(categoryResponse) + return contract.BuildSuccessResponse(contractResponse) +} + +func (s *CategoryServiceImpl) DeleteCategory(ctx context.Context, id uuid.UUID) *contract.Response { + err := s.categoryProcessor.DeleteCategory(ctx, id) + if err != nil { + errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.CategoryServiceEntity, err.Error()) + return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) + } + + return contract.BuildSuccessResponse(map[string]interface{}{ + "message": "Category deleted successfully", + }) +} + +func (s *CategoryServiceImpl) GetCategoryByID(ctx context.Context, id uuid.UUID) *contract.Response { + categoryResponse, err := s.categoryProcessor.GetCategoryByID(ctx, id) + if err != nil { + errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.CategoryServiceEntity, err.Error()) + return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) + } + + contractResponse := transformer.CategoryModelResponseToResponse(categoryResponse) + return contract.BuildSuccessResponse(contractResponse) +} + +func (s *CategoryServiceImpl) ListCategories(ctx context.Context, req *contract.ListCategoriesRequest) *contract.Response { + // Build filters + filters := make(map[string]interface{}) + if req.OrganizationID != nil { + filters["organization_id"] = *req.OrganizationID + } + if req.BusinessType != "" { + filters["business_type"] = req.BusinessType + } + if req.Search != "" { + filters["search"] = req.Search + } + + categories, totalCount, err := s.categoryProcessor.ListCategories(ctx, filters, req.Page, req.Limit) + if err != nil { + errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.CategoryServiceEntity, err.Error()) + return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) + } + + // Convert to contract responses + contractResponses := transformer.CategoriesToResponses(categories) + + // Calculate total pages + totalPages := totalCount / req.Limit + if totalCount%req.Limit > 0 { + totalPages++ + } + + listResponse := &contract.ListCategoriesResponse{ + Categories: contractResponses, + TotalCount: totalCount, + Page: req.Page, + Limit: req.Limit, + TotalPages: totalPages, + } + + return contract.BuildSuccessResponse(listResponse) +} diff --git a/internal/service/customer_service.go b/internal/service/customer_service.go new file mode 100644 index 0000000..8fcc506 --- /dev/null +++ b/internal/service/customer_service.go @@ -0,0 +1,111 @@ +package service + +import ( + "apskel-pos-be/internal/contract" + "apskel-pos-be/internal/processor" + "apskel-pos-be/internal/transformer" + "context" + + "github.com/google/uuid" +) + +type CustomerService interface { + CreateCustomer(ctx context.Context, req *contract.CreateCustomerRequest, organizationID uuid.UUID) (*contract.CustomerResponse, error) + GetCustomer(ctx context.Context, customerID, organizationID uuid.UUID) (*contract.CustomerResponse, error) + ListCustomers(ctx context.Context, query *contract.ListCustomersRequest, organizationID uuid.UUID) (*contract.PaginatedCustomerResponse, error) + UpdateCustomer(ctx context.Context, customerID, organizationID uuid.UUID, req *contract.UpdateCustomerRequest) (*contract.CustomerResponse, error) + DeleteCustomer(ctx context.Context, customerID, organizationID uuid.UUID) error + SetDefaultCustomer(ctx context.Context, customerID, organizationID uuid.UUID) (*contract.CustomerResponse, error) + GetDefaultCustomer(ctx context.Context, organizationID uuid.UUID) (*contract.CustomerResponse, error) + EnsureDefaultCustomer(ctx context.Context, organizationID uuid.UUID) (*contract.CustomerResponse, error) +} + +type CustomerServiceImpl struct { + customerProcessor *processor.CustomerProcessor +} + +func NewCustomerService(customerProcessor *processor.CustomerProcessor) *CustomerServiceImpl { + return &CustomerServiceImpl{ + customerProcessor: customerProcessor, + } +} + +func (s *CustomerServiceImpl) CreateCustomer(ctx context.Context, req *contract.CreateCustomerRequest, organizationID uuid.UUID) (*contract.CustomerResponse, error) { + modelReq := transformer.CreateCustomerRequestToModel(req) + + customerResponse, err := s.customerProcessor.CreateCustomer(ctx, modelReq, organizationID) + if err != nil { + return nil, err + } + + contractResponse := transformer.CustomerModelToResponse(customerResponse) + return contractResponse, nil +} + +func (s *CustomerServiceImpl) GetCustomer(ctx context.Context, customerID, organizationID uuid.UUID) (*contract.CustomerResponse, error) { + customerResponse, err := s.customerProcessor.GetCustomer(ctx, customerID, organizationID) + if err != nil { + return nil, err + } + + contractResponse := transformer.CustomerModelToResponse(customerResponse) + return contractResponse, nil +} + +func (s *CustomerServiceImpl) ListCustomers(ctx context.Context, query *contract.ListCustomersRequest, organizationID uuid.UUID) (*contract.PaginatedCustomerResponse, error) { + modelQuery := transformer.ListCustomersRequestToModel(query) + + customersResponse, err := s.customerProcessor.ListCustomers(ctx, modelQuery, organizationID) + if err != nil { + return nil, err + } + + contractResponse := transformer.PaginatedCustomerResponseToContract(customersResponse) + return contractResponse, nil +} + +func (s *CustomerServiceImpl) UpdateCustomer(ctx context.Context, customerID, organizationID uuid.UUID, req *contract.UpdateCustomerRequest) (*contract.CustomerResponse, error) { + modelReq := transformer.UpdateCustomerRequestToModel(req) + + customerResponse, err := s.customerProcessor.UpdateCustomer(ctx, customerID, organizationID, modelReq) + if err != nil { + return nil, err + } + + contractResponse := transformer.CustomerModelToResponse(customerResponse) + return contractResponse, nil +} + +func (s *CustomerServiceImpl) DeleteCustomer(ctx context.Context, customerID, organizationID uuid.UUID) error { + return s.customerProcessor.DeleteCustomer(ctx, customerID, organizationID) +} + +func (s *CustomerServiceImpl) SetDefaultCustomer(ctx context.Context, customerID, organizationID uuid.UUID) (*contract.CustomerResponse, error) { + customerResponse, err := s.customerProcessor.SetDefaultCustomer(ctx, customerID, organizationID) + if err != nil { + return nil, err + } + + contractResponse := transformer.CustomerModelToResponse(customerResponse) + return contractResponse, nil +} + +func (s *CustomerServiceImpl) GetDefaultCustomer(ctx context.Context, organizationID uuid.UUID) (*contract.CustomerResponse, error) { + customerResponse, err := s.customerProcessor.GetDefaultCustomer(ctx, organizationID) + if err != nil { + return nil, err + } + + contractResponse := transformer.CustomerModelToResponse(customerResponse) + return contractResponse, nil +} + +func (s *CustomerServiceImpl) EnsureDefaultCustomer(ctx context.Context, organizationID uuid.UUID) (*contract.CustomerResponse, error) { + customerResponse, err := s.customerProcessor.EnsureDefaultCustomer(ctx, organizationID) + if err != nil { + return nil, err + } + + contractResponse := transformer.CustomerModelToResponse(customerResponse) + return contractResponse, nil +} diff --git a/internal/service/file_service.go b/internal/service/file_service.go new file mode 100644 index 0000000..be07ad6 --- /dev/null +++ b/internal/service/file_service.go @@ -0,0 +1,166 @@ +package service + +import ( + "context" + "fmt" + + "apskel-pos-be/internal/models" + "apskel-pos-be/internal/processor" + + "mime/multipart" + + "github.com/google/uuid" +) + +type FileService interface { + UploadFile(ctx context.Context, file *multipart.FileHeader, req *models.UploadFileRequest, organizationID, userID uuid.UUID) (*models.FileResponse, error) + GetFileByID(ctx context.Context, id uuid.UUID) (*models.FileResponse, error) + UpdateFile(ctx context.Context, id uuid.UUID, req *models.UpdateFileRequest) (*models.FileResponse, error) + ListFiles(ctx context.Context, req *models.ListFilesRequest) (*models.ListFilesResponse, error) + GetFileByOrganizationID(ctx context.Context, organizationID uuid.UUID) ([]*models.FileResponse, error) + GetFileByUserID(ctx context.Context, userID uuid.UUID) ([]*models.FileResponse, error) +} + +type FileServiceImpl struct { + fileProcessor processor.FileProcessor +} + +func NewFileServiceImpl(fileProcessor processor.FileProcessor) *FileServiceImpl { + return &FileServiceImpl{ + fileProcessor: fileProcessor, + } +} + +func (s *FileServiceImpl) UploadFile(ctx context.Context, file *multipart.FileHeader, req *models.UploadFileRequest, organizationID, userID uuid.UUID) (*models.FileResponse, error) { + if err := s.validateUploadFileRequest(req); err != nil { + return nil, fmt.Errorf("validation error: %w", err) + } + + if organizationID == uuid.Nil { + return nil, fmt.Errorf("organization ID is required") + } + if userID == uuid.Nil { + return nil, fmt.Errorf("user ID is required") + } + + // Process file upload + response, err := s.fileProcessor.UploadFile(ctx, file, req, organizationID, userID) + if err != nil { + return nil, fmt.Errorf("failed to upload file: %w", err) + } + + return response, nil +} + +func (s *FileServiceImpl) GetFileByID(ctx context.Context, id uuid.UUID) (*models.FileResponse, error) { + if id == uuid.Nil { + return nil, fmt.Errorf("invalid file ID") + } + + response, err := s.fileProcessor.GetFileByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("failed to get file: %w", err) + } + + return response, nil +} + +func (s *FileServiceImpl) UpdateFile(ctx context.Context, id uuid.UUID, req *models.UpdateFileRequest) (*models.FileResponse, error) { + // Validate inputs + if id == uuid.Nil { + return nil, fmt.Errorf("invalid file ID") + } + + // Validate request + if err := s.validateUpdateFileRequest(req); err != nil { + return nil, fmt.Errorf("validation error: %w", err) + } + + // Process file update + response, err := s.fileProcessor.UpdateFile(ctx, id, req) + if err != nil { + return nil, fmt.Errorf("failed to update file: %w", err) + } + + return response, nil +} + +func (s *FileServiceImpl) ListFiles(ctx context.Context, req *models.ListFilesRequest) (*models.ListFilesResponse, error) { + // Validate request + if err := s.validateListFilesRequest(req); err != nil { + return nil, fmt.Errorf("validation error: %w", err) + } + + // Process file listing + response, err := s.fileProcessor.ListFiles(ctx, req) + if err != nil { + return nil, fmt.Errorf("failed to list files: %w", err) + } + + return response, nil +} + +func (s *FileServiceImpl) GetFileByOrganizationID(ctx context.Context, organizationID uuid.UUID) ([]*models.FileResponse, error) { + // Validate organization ID + if organizationID == uuid.Nil { + return nil, fmt.Errorf("invalid organization ID") + } + + // Get files by organization + response, err := s.fileProcessor.GetFileByOrganizationID(ctx, organizationID) + if err != nil { + return nil, fmt.Errorf("failed to get files by organization: %w", err) + } + + return response, nil +} + +func (s *FileServiceImpl) GetFileByUserID(ctx context.Context, userID uuid.UUID) ([]*models.FileResponse, error) { + // Validate user ID + if userID == uuid.Nil { + return nil, fmt.Errorf("invalid user ID") + } + + // Get files by user + response, err := s.fileProcessor.GetFileByUserID(ctx, userID) + if err != nil { + return nil, fmt.Errorf("failed to get files by user: %w", err) + } + + return response, nil +} + +// Validation methods +func (s *FileServiceImpl) validateUploadFileRequest(req *models.UploadFileRequest) error { + if req == nil { + return fmt.Errorf("request cannot be nil") + } + + // File type validation is handled in the processor + return nil +} + +func (s *FileServiceImpl) validateUpdateFileRequest(req *models.UpdateFileRequest) error { + if req == nil { + return fmt.Errorf("request cannot be nil") + } + + // Additional validation can be added here if needed + return nil +} + +func (s *FileServiceImpl) validateListFilesRequest(req *models.ListFilesRequest) error { + if req == nil { + return fmt.Errorf("request cannot be nil") + } + + if req.Page < 1 { + return fmt.Errorf("page must be greater than 0") + } + + if req.Limit < 1 || req.Limit > 100 { + return fmt.Errorf("limit must be between 1 and 100") + } + + return nil +} diff --git a/internal/service/inventory_service.go b/internal/service/inventory_service.go new file mode 100644 index 0000000..5a8284e --- /dev/null +++ b/internal/service/inventory_service.go @@ -0,0 +1,166 @@ +package service + +import ( + "context" + + "apskel-pos-be/internal/appcontext" + "apskel-pos-be/internal/constants" + "apskel-pos-be/internal/contract" + "apskel-pos-be/internal/processor" + "apskel-pos-be/internal/transformer" + + "github.com/google/uuid" +) + +type InventoryService interface { + CreateInventory(ctx context.Context, apctx *appcontext.ContextInfo, req *contract.CreateInventoryRequest) *contract.Response + UpdateInventory(ctx context.Context, id uuid.UUID, req *contract.UpdateInventoryRequest) *contract.Response + DeleteInventory(ctx context.Context, id uuid.UUID) *contract.Response + GetInventoryByID(ctx context.Context, id uuid.UUID) *contract.Response + ListInventory(ctx context.Context, req *contract.ListInventoryRequest) *contract.Response + AdjustInventory(ctx context.Context, req *contract.AdjustInventoryRequest) *contract.Response + GetLowStockItems(ctx context.Context, outletID uuid.UUID) *contract.Response + GetZeroStockItems(ctx context.Context, outletID uuid.UUID) *contract.Response +} + +type InventoryServiceImpl struct { + inventoryProcessor processor.InventoryProcessor +} + +func NewInventoryService(inventoryProcessor processor.InventoryProcessor) *InventoryServiceImpl { + return &InventoryServiceImpl{ + inventoryProcessor: inventoryProcessor, + } +} + +func (s *InventoryServiceImpl) CreateInventory(ctx context.Context, apctx *appcontext.ContextInfo, req *contract.CreateInventoryRequest) *contract.Response { + modelReq := transformer.CreateInventoryRequestToModel(req) + + inventoryResponse, err := s.inventoryProcessor.CreateInventory(ctx, modelReq) + if err != nil { + errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.InventoryServiceEntity, err.Error()) + return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) + } + + contractResponse := transformer.InventoryModelResponseToResponse(inventoryResponse) + return contract.BuildSuccessResponse(contractResponse) +} + +func (s *InventoryServiceImpl) UpdateInventory(ctx context.Context, id uuid.UUID, req *contract.UpdateInventoryRequest) *contract.Response { + modelReq := transformer.UpdateInventoryRequestToModel(req) + + inventoryResponse, err := s.inventoryProcessor.UpdateInventory(ctx, id, modelReq) + if err != nil { + errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.InventoryServiceEntity, err.Error()) + return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) + } + + contractResponse := transformer.InventoryModelResponseToResponse(inventoryResponse) + return contract.BuildSuccessResponse(contractResponse) +} + +func (s *InventoryServiceImpl) DeleteInventory(ctx context.Context, id uuid.UUID) *contract.Response { + err := s.inventoryProcessor.DeleteInventory(ctx, id) + if err != nil { + errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.InventoryServiceEntity, err.Error()) + return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) + } + + return contract.BuildSuccessResponse(map[string]interface{}{ + "message": "Inventory deleted successfully", + }) +} + +func (s *InventoryServiceImpl) GetInventoryByID(ctx context.Context, id uuid.UUID) *contract.Response { + inventoryResponse, err := s.inventoryProcessor.GetInventoryByID(ctx, id) + if err != nil { + errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.InventoryServiceEntity, err.Error()) + return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) + } + + contractResponse := transformer.InventoryModelResponseToResponse(inventoryResponse) + return contract.BuildSuccessResponse(contractResponse) +} + +func (s *InventoryServiceImpl) ListInventory(ctx context.Context, req *contract.ListInventoryRequest) *contract.Response { + // Build filters + filters := make(map[string]interface{}) + if req.OutletID != nil { + filters["outlet_id"] = *req.OutletID + } + if req.ProductID != nil { + filters["product_id"] = *req.ProductID + } + if req.CategoryID != nil { + filters["category_id"] = *req.CategoryID + } + if req.LowStockOnly != nil && *req.LowStockOnly { + filters["low_stock"] = true + } + if req.ZeroStockOnly != nil && *req.ZeroStockOnly { + filters["zero_stock"] = true + } + if req.Search != "" { + filters["search"] = req.Search + } + + inventory, totalCount, err := s.inventoryProcessor.ListInventory(ctx, filters, req.Page, req.Limit) + if err != nil { + errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.InventoryServiceEntity, err.Error()) + return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) + } + + // Convert to contract responses + contractResponses := transformer.InventoryToResponses(inventory) + + // Calculate total pages + totalPages := totalCount / req.Limit + if totalCount%req.Limit > 0 { + totalPages++ + } + + listResponse := &contract.ListInventoryResponse{ + Inventory: contractResponses, + TotalCount: totalCount, + Page: req.Page, + Limit: req.Limit, + TotalPages: totalPages, + } + + return contract.BuildSuccessResponse(listResponse) +} + +func (s *InventoryServiceImpl) AdjustInventory(ctx context.Context, req *contract.AdjustInventoryRequest) *contract.Response { + modelReq := transformer.AdjustInventoryRequestToModel(req) + + inventoryResponse, err := s.inventoryProcessor.AdjustInventory(ctx, req.ProductID, req.OutletID, modelReq) + if err != nil { + errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.InventoryServiceEntity, err.Error()) + return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) + } + + contractResponse := transformer.InventoryModelResponseToResponse(inventoryResponse) + return contract.BuildSuccessResponse(contractResponse) +} + +func (s *InventoryServiceImpl) GetLowStockItems(ctx context.Context, outletID uuid.UUID) *contract.Response { + inventory, err := s.inventoryProcessor.GetLowStockItems(ctx, outletID) + if err != nil { + errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.InventoryServiceEntity, err.Error()) + return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) + } + + contractResponses := transformer.InventoryToResponses(inventory) + return contract.BuildSuccessResponse(contractResponses) +} + +func (s *InventoryServiceImpl) GetZeroStockItems(ctx context.Context, outletID uuid.UUID) *contract.Response { + inventory, err := s.inventoryProcessor.GetZeroStockItems(ctx, outletID) + if err != nil { + errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.InventoryServiceEntity, err.Error()) + return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) + } + + contractResponses := transformer.InventoryToResponses(inventory) + return contract.BuildSuccessResponse(contractResponses) +} diff --git a/internal/service/order_service.go b/internal/service/order_service.go new file mode 100644 index 0000000..c1f0fca --- /dev/null +++ b/internal/service/order_service.go @@ -0,0 +1,386 @@ +package service + +import ( + "context" + "fmt" + + "apskel-pos-be/internal/models" + "apskel-pos-be/internal/processor" + + "github.com/google/uuid" +) + +type OrderService interface { + CreateOrder(ctx context.Context, req *models.CreateOrderRequest, organizationID uuid.UUID) (*models.OrderResponse, error) + AddToOrder(ctx context.Context, orderID uuid.UUID, req *models.AddToOrderRequest) (*models.AddToOrderResponse, error) + UpdateOrder(ctx context.Context, id uuid.UUID, req *models.UpdateOrderRequest) (*models.OrderResponse, error) + GetOrderByID(ctx context.Context, id uuid.UUID) (*models.OrderResponse, error) + ListOrders(ctx context.Context, req *models.ListOrdersRequest) (*models.ListOrdersResponse, error) + VoidOrder(ctx context.Context, req *models.VoidOrderRequest, voidedBy uuid.UUID) error + RefundOrder(ctx context.Context, id uuid.UUID, req *models.RefundOrderRequest, refundedBy uuid.UUID) error + CreatePayment(ctx context.Context, req *models.CreatePaymentRequest) (*models.PaymentResponse, error) + RefundPayment(ctx context.Context, paymentID uuid.UUID, refundAmount float64, reason string, refundedBy uuid.UUID) error + SetOrderCustomer(ctx context.Context, orderID uuid.UUID, req *models.SetOrderCustomerRequest, organizationID uuid.UUID) (*models.SetOrderCustomerResponse, error) +} + +type OrderServiceImpl struct { + orderProcessor processor.OrderProcessor +} + +func NewOrderServiceImpl(orderProcessor processor.OrderProcessor) *OrderServiceImpl { + return &OrderServiceImpl{ + orderProcessor: orderProcessor, + } +} + +func (s *OrderServiceImpl) CreateOrder(ctx context.Context, req *models.CreateOrderRequest, organizationID uuid.UUID) (*models.OrderResponse, error) { + if err := s.validateCreateOrderRequest(req, organizationID); err != nil { + return nil, fmt.Errorf("validation error: %w", err) + } + + response, err := s.orderProcessor.CreateOrder(ctx, req, organizationID) + if err != nil { + return nil, fmt.Errorf("failed to create order: %w", err) + } + + return response, nil +} + +func (s *OrderServiceImpl) AddToOrder(ctx context.Context, orderID uuid.UUID, req *models.AddToOrderRequest) (*models.AddToOrderResponse, error) { + // Validate inputs + if orderID == uuid.Nil { + return nil, fmt.Errorf("invalid order ID") + } + + // Validate request + if err := s.validateAddToOrderRequest(req); err != nil { + return nil, fmt.Errorf("validation error: %w", err) + } + + // Process adding items to order + response, err := s.orderProcessor.AddToOrder(ctx, orderID, req) + if err != nil { + return nil, fmt.Errorf("failed to add items to order: %w", err) + } + + return response, nil +} + +func (s *OrderServiceImpl) UpdateOrder(ctx context.Context, id uuid.UUID, req *models.UpdateOrderRequest) (*models.OrderResponse, error) { + // Validate request + if err := s.validateUpdateOrderRequest(req); err != nil { + return nil, fmt.Errorf("validation error: %w", err) + } + + // Process order update + response, err := s.orderProcessor.UpdateOrder(ctx, id, req) + if err != nil { + return nil, fmt.Errorf("failed to update order: %w", err) + } + + return response, nil +} + +func (s *OrderServiceImpl) GetOrderByID(ctx context.Context, id uuid.UUID) (*models.OrderResponse, error) { + if id == uuid.Nil { + return nil, fmt.Errorf("invalid order ID") + } + + response, err := s.orderProcessor.GetOrderByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("failed to get order: %w", err) + } + + return response, nil +} + +func (s *OrderServiceImpl) ListOrders(ctx context.Context, req *models.ListOrdersRequest) (*models.ListOrdersResponse, error) { + // Validate request + if err := s.validateListOrdersRequest(req); err != nil { + return nil, fmt.Errorf("validation error: %w", err) + } + + // Process order listing + response, err := s.orderProcessor.ListOrders(ctx, req) + if err != nil { + return nil, fmt.Errorf("failed to list orders: %w", err) + } + + return response, nil +} + +func (s *OrderServiceImpl) VoidOrder(ctx context.Context, req *models.VoidOrderRequest, voidedBy uuid.UUID) error { + if req.OrderID == uuid.Nil { + return fmt.Errorf("invalid order ID") + } + + if voidedBy == uuid.Nil { + return fmt.Errorf("invalid user ID") + } + + if err := s.orderProcessor.VoidOrder(ctx, req, voidedBy); err != nil { + return fmt.Errorf("failed to void order: %w", err) + } + + return nil +} + +func (s *OrderServiceImpl) RefundOrder(ctx context.Context, id uuid.UUID, req *models.RefundOrderRequest, refundedBy uuid.UUID) error { + // Validate inputs + if id == uuid.Nil { + return fmt.Errorf("invalid order ID") + } + if refundedBy == uuid.Nil { + return fmt.Errorf("invalid user ID") + } + + // Validate refund request + if err := s.validateRefundOrderRequest(req); err != nil { + return fmt.Errorf("validation error: %w", err) + } + + // Process order refund + if err := s.orderProcessor.RefundOrder(ctx, id, req, refundedBy); err != nil { + return fmt.Errorf("failed to refund order: %w", err) + } + + return nil +} + +func (s *OrderServiceImpl) CreatePayment(ctx context.Context, req *models.CreatePaymentRequest) (*models.PaymentResponse, error) { + if err := s.validateCreatePaymentRequest(req); err != nil { + return nil, fmt.Errorf("validation error: %w", err) + } + + // Process payment creation + response, err := s.orderProcessor.CreatePayment(ctx, req) + if err != nil { + return nil, fmt.Errorf("failed to create payment: %w", err) + } + + return response, nil +} + +func (s *OrderServiceImpl) RefundPayment(ctx context.Context, paymentID uuid.UUID, refundAmount float64, reason string, refundedBy uuid.UUID) error { + // Validate inputs + if paymentID == uuid.Nil { + return fmt.Errorf("invalid payment ID") + } + if refundAmount <= 0 { + return fmt.Errorf("refund amount must be greater than zero") + } + if refundedBy == uuid.Nil { + return fmt.Errorf("invalid user ID") + } + + // Process payment refund + if err := s.orderProcessor.RefundPayment(ctx, paymentID, refundAmount, reason, refundedBy); err != nil { + return fmt.Errorf("failed to refund payment: %w", err) + } + + return nil +} + +func (s *OrderServiceImpl) SetOrderCustomer(ctx context.Context, orderID uuid.UUID, req *models.SetOrderCustomerRequest, organizationID uuid.UUID) (*models.SetOrderCustomerResponse, error) { + // Validate inputs + if orderID == uuid.Nil { + return nil, fmt.Errorf("invalid order ID") + } + + if organizationID == uuid.Nil { + return nil, fmt.Errorf("invalid organization ID") + } + + // Validate request + if err := s.validateSetOrderCustomerRequest(req); err != nil { + return nil, fmt.Errorf("validation error: %w", err) + } + + // Process setting customer for order + response, err := s.orderProcessor.SetOrderCustomer(ctx, orderID, req, organizationID) + if err != nil { + return nil, fmt.Errorf("failed to set customer for order: %w", err) + } + + return response, nil +} + +func (s *OrderServiceImpl) validateAddToOrderRequest(req *models.AddToOrderRequest) error { + if req == nil { + return fmt.Errorf("request cannot be nil") + } + + if len(req.OrderItems) == 0 { + return fmt.Errorf("must add at least one item") + } + + for i, item := range req.OrderItems { + if item.ProductID == uuid.Nil { + return fmt.Errorf("product ID is required for item %d", i+1) + } + + if item.Quantity <= 0 { + return fmt.Errorf("quantity must be greater than zero for item %d", i+1) + } + + if item.UnitPrice != nil && *item.UnitPrice < 0 { + return fmt.Errorf("unit price cannot be negative for item %d", i+1) + } + } + + return nil +} + +func (s *OrderServiceImpl) validateCreateOrderRequest(req *models.CreateOrderRequest, organizationID uuid.UUID) error { + if req == nil { + return fmt.Errorf("request cannot be nil") + } + + if organizationID == uuid.Nil { + return fmt.Errorf("organization ID is required") + } + + if req.OutletID == uuid.Nil { + return fmt.Errorf("outlet ID is required") + } + + if req.UserID == uuid.Nil { + return fmt.Errorf("user ID is required") + } + + if len(req.OrderItems) == 0 { + return fmt.Errorf("order must have at least one item") + } + + for i, item := range req.OrderItems { + if item.ProductID == uuid.Nil { + return fmt.Errorf("product ID is required for item %d", i+1) + } + + if item.Quantity <= 0 { + return fmt.Errorf("quantity must be greater than zero for item %d", i+1) + } + + if item.UnitPrice != nil && *item.UnitPrice < 0 { + return fmt.Errorf("unit price cannot be negative for item %d", i+1) + } + } + + return nil +} + +func (s *OrderServiceImpl) validateUpdateOrderRequest(req *models.UpdateOrderRequest) error { + if req == nil { + return fmt.Errorf("request cannot be nil") + } + + if req.DiscountAmount != nil && *req.DiscountAmount < 0 { + return fmt.Errorf("discount amount cannot be negative") + } + + return nil +} + +func (s *OrderServiceImpl) validateListOrdersRequest(req *models.ListOrdersRequest) error { + if req == nil { + return fmt.Errorf("request cannot be nil") + } + + if req.Page < 1 { + return fmt.Errorf("page must be greater than zero") + } + + if req.Limit < 1 || req.Limit > 100 { + return fmt.Errorf("limit must be between 1 and 100") + } + + return nil +} + +func (s *OrderServiceImpl) validateRefundOrderRequest(req *models.RefundOrderRequest) error { + if req == nil { + return fmt.Errorf("request cannot be nil") + } + + // Must have either refund amount or order items + if req.RefundAmount == nil && len(req.OrderItems) == 0 { + return fmt.Errorf("must specify either refund amount or order items to refund") + } + + // Cannot have both refund amount and order items + if req.RefundAmount != nil && len(req.OrderItems) > 0 { + return fmt.Errorf("cannot specify both refund amount and order items") + } + + if req.RefundAmount != nil && *req.RefundAmount <= 0 { + return fmt.Errorf("refund amount must be greater than zero") + } + + // Validate order items if provided + for i, item := range req.OrderItems { + if item.OrderItemID == uuid.Nil { + return fmt.Errorf("order item ID is required for item %d", i+1) + } + + if item.RefundQuantity < 0 { + return fmt.Errorf("refund quantity cannot be negative for item %d", i+1) + } + + if item.RefundAmount != nil && *item.RefundAmount < 0 { + return fmt.Errorf("refund amount cannot be negative for item %d", i+1) + } + } + + return nil +} + +func (s *OrderServiceImpl) validateCreatePaymentRequest(req *models.CreatePaymentRequest) error { + if req == nil { + return fmt.Errorf("request cannot be nil") + } + + if req.OrderID == uuid.Nil { + return fmt.Errorf("order ID is required") + } + + if req.PaymentMethodID == uuid.Nil { + return fmt.Errorf("payment method ID is required") + } + + if req.Amount <= 0 { + return fmt.Errorf("payment amount must be greater than zero") + } + + if len(req.PaymentOrderItems) > 0 { + totalItemAmount := float64(0) + for i, item := range req.PaymentOrderItems { + if item.OrderItemID == uuid.Nil { + return fmt.Errorf("order item ID is required for payment item %d", i+1) + } + + if item.Amount <= 0 { + return fmt.Errorf("payment item amount must be greater than zero for item %d", i+1) + } + + totalItemAmount += item.Amount + } + + if totalItemAmount != req.Amount { + return fmt.Errorf("sum of payment item amounts must equal total payment amount") + } + } + + return nil +} + +func (s *OrderServiceImpl) validateSetOrderCustomerRequest(req *models.SetOrderCustomerRequest) error { + if req == nil { + return fmt.Errorf("request cannot be nil") + } + + if req.CustomerID == uuid.Nil { + return fmt.Errorf("customer ID is required") + } + + return nil +} diff --git a/internal/service/organization_service.go b/internal/service/organization_service.go new file mode 100644 index 0000000..34df3b3 --- /dev/null +++ b/internal/service/organization_service.go @@ -0,0 +1,119 @@ +package service + +import ( + "apskel-pos-be/internal/appcontext" + "context" + + "apskel-pos-be/internal/constants" + "apskel-pos-be/internal/contract" + "apskel-pos-be/internal/processor" + "apskel-pos-be/internal/transformer" + + "github.com/google/uuid" +) + +type OrganizationService interface { + CreateOrganization(ctx context.Context, apctx *appcontext.ContextInfo, req *contract.CreateOrganizationRequest) *contract.Response + UpdateOrganization(ctx context.Context, id uuid.UUID, req *contract.UpdateOrganizationRequest) *contract.Response + DeleteOrganization(ctx context.Context, id uuid.UUID) *contract.Response + GetOrganizationByID(ctx context.Context, id uuid.UUID) *contract.Response + ListOrganizations(ctx context.Context, req *contract.ListOrganizationsRequest) *contract.Response +} + +type OrganizationServiceImpl struct { + organizationProcessor processor.OrganizationProcessor +} + +func NewOrganizationService(organizationProcessor processor.OrganizationProcessor) *OrganizationServiceImpl { + return &OrganizationServiceImpl{ + organizationProcessor: organizationProcessor, + } +} + +func (s *OrganizationServiceImpl) CreateOrganization(ctx context.Context, apctx *appcontext.ContextInfo, req *contract.CreateOrganizationRequest) *contract.Response { + modelReq := transformer.CreateOrganizationRequestToModel(req) + + organizationResponse, err := s.organizationProcessor.CreateOrganization(ctx, modelReq) + if err != nil { + errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.OrganizationServiceEntity, err.Error()) + return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) + } + + contractResponse := transformer.CreateOrganizationResponseToContract(organizationResponse) + return contract.BuildSuccessResponse(contractResponse) +} + +func (s *OrganizationServiceImpl) UpdateOrganization(ctx context.Context, id uuid.UUID, req *contract.UpdateOrganizationRequest) *contract.Response { + modelReq := transformer.UpdateOrganizationRequestToModel(req) + + organizationResponse, err := s.organizationProcessor.UpdateOrganization(ctx, id, modelReq) + if err != nil { + errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.OrganizationServiceEntity, err.Error()) + return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) + } + + contractResponse := transformer.OrganizationModelResponseToResponse(organizationResponse) + return contract.BuildSuccessResponse(contractResponse) +} + +func (s *OrganizationServiceImpl) DeleteOrganization(ctx context.Context, id uuid.UUID) *contract.Response { + err := s.organizationProcessor.DeleteOrganization(ctx, id) + if err != nil { + errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.OrganizationServiceEntity, err.Error()) + return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) + } + + return contract.BuildSuccessResponse(map[string]interface{}{ + "message": "Organization deleted successfully", + }) +} + +func (s *OrganizationServiceImpl) GetOrganizationByID(ctx context.Context, id uuid.UUID) *contract.Response { + organizationResponse, err := s.organizationProcessor.GetOrganizationByID(ctx, id) + if err != nil { + errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.OrganizationServiceEntity, err.Error()) + return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) + } + + contractResponse := transformer.OrganizationModelResponseToResponse(organizationResponse) + return contract.BuildSuccessResponse(contractResponse) +} + +func (s *OrganizationServiceImpl) ListOrganizations(ctx context.Context, req *contract.ListOrganizationsRequest) *contract.Response { + filters := make(map[string]interface{}) + if req.PlanType != "" { + filters["plan_type"] = req.PlanType + } + if req.Search != "" { + filters["name"] = req.Search + } + + organizations, totalCount, err := s.organizationProcessor.ListOrganizations(ctx, filters, req.Page, req.Limit) + if err != nil { + errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.OrganizationServiceEntity, err.Error()) + return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) + } + + contractResponses := make([]contract.OrganizationResponse, len(organizations)) + for i, org := range organizations { + response := transformer.OrganizationModelResponseToResponse(&org) + if response != nil { + contractResponses[i] = *response + } + } + + totalPages := totalCount / req.Limit + if totalCount%req.Limit > 0 { + totalPages++ + } + + listResponse := &contract.ListOrganizationsResponse{ + Organizations: contractResponses, + TotalCount: totalCount, + Page: req.Page, + Limit: req.Limit, + TotalPages: totalPages, + } + + return contract.BuildSuccessResponse(listResponse) +} diff --git a/internal/service/outlet_service.go b/internal/service/outlet_service.go new file mode 100644 index 0000000..cdd95aa --- /dev/null +++ b/internal/service/outlet_service.go @@ -0,0 +1,106 @@ +package service + +import ( + "apskel-pos-be/internal/constants" + "apskel-pos-be/internal/contract" + "apskel-pos-be/internal/processor" + "apskel-pos-be/internal/transformer" + "context" + + "github.com/google/uuid" +) + +type OutletService interface { + ListOutlets(ctx context.Context, req *contract.ListOutletsRequest) *contract.Response + GetOutletByID(ctx context.Context, organizationID, outletID uuid.UUID) *contract.Response + CreateOutlet(ctx context.Context, req *contract.CreateOutletRequest) *contract.Response + UpdateOutlet(ctx context.Context, outletID uuid.UUID, req *contract.UpdateOutletRequest) *contract.Response + DeleteOutlet(ctx context.Context, outletID uuid.UUID) *contract.Response +} + +type OutletServiceImpl struct { + outletProcessor processor.OutletProcessor +} + +func NewOutletService(outletProcessor processor.OutletProcessor) *OutletServiceImpl { + return &OutletServiceImpl{ + outletProcessor: outletProcessor, + } +} + +func (s *OutletServiceImpl) ListOutlets(ctx context.Context, req *contract.ListOutletsRequest) *contract.Response { + // Validate request + if req.Page < 1 { + req.Page = 1 + } + if req.Limit < 1 { + req.Limit = 10 + } + if req.Limit > 100 { + req.Limit = 100 + } + + outlets, total, err := s.outletProcessor.ListOutletsByOrganization(ctx, req.OrganizationID, req.Page, req.Limit) + if err != nil { + errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.OutletServiceEntity, err.Error()) + return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) + } + + contractOutlets := make([]contract.OutletResponse, len(outlets)) + for i, outlet := range outlets { + contractOutlets[i] = transformer.OutletModelResponseToResponse(outlet) + } + + // Create paginated response + response := transformer.CreateListOutletsResponse(contractOutlets, int(total), req.Page, req.Limit) + return contract.BuildSuccessResponse(response) +} + +func (s *OutletServiceImpl) GetOutletByID(ctx context.Context, organizationID, outletID uuid.UUID) *contract.Response { + outlet, err := s.outletProcessor.GetOutletByID(ctx, organizationID, outletID) + if err != nil { + errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.OutletServiceEntity, err.Error()) + return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) + } + + contractResponse := transformer.OutletModelResponseToResponse(outlet) + return contract.BuildSuccessResponse(contractResponse) +} + +func (s *OutletServiceImpl) CreateOutlet(ctx context.Context, req *contract.CreateOutletRequest) *contract.Response { + modelReq := transformer.CreateOutletRequestToModel(req) + + outlet, err := s.outletProcessor.CreateOutlet(ctx, modelReq) + if err != nil { + errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.OutletServiceEntity, err.Error()) + return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) + } + + contractResponse := transformer.OutletModelResponseToResponse(outlet) + return contract.BuildSuccessResponse(contractResponse) +} + +func (s *OutletServiceImpl) UpdateOutlet(ctx context.Context, outletID uuid.UUID, req *contract.UpdateOutletRequest) *contract.Response { + modelReq := transformer.UpdateOutletRequestToModel(req) + + outlet, err := s.outletProcessor.UpdateOutlet(ctx, outletID, modelReq) + if err != nil { + errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.OutletServiceEntity, err.Error()) + return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) + } + + contractResponse := transformer.OutletModelResponseToResponse(outlet) + return contract.BuildSuccessResponse(contractResponse) +} + +func (s *OutletServiceImpl) DeleteOutlet(ctx context.Context, outletID uuid.UUID) *contract.Response { + err := s.outletProcessor.DeleteOutlet(ctx, outletID) + if err != nil { + errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.OutletServiceEntity, err.Error()) + return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) + } + + return contract.BuildSuccessResponse(map[string]interface{}{ + "message": "Outlet deleted successfully", + }) +} diff --git a/internal/service/outlet_setting_service.go b/internal/service/outlet_setting_service.go new file mode 100644 index 0000000..7c181e3 --- /dev/null +++ b/internal/service/outlet_setting_service.go @@ -0,0 +1,52 @@ +package service + +import ( + "apskel-pos-be/internal/models" + "apskel-pos-be/internal/processor" + "context" + + "github.com/google/uuid" +) + +type OutletSettingService interface { + CreateSetting(ctx context.Context, req *models.CreateOutletSettingRequest) (*models.OutletSettingResponse, error) + UpdateSetting(ctx context.Context, outletID uuid.UUID, key string, req *models.UpdateOutletSettingRequest) (*models.OutletSettingResponse, error) + GetSetting(ctx context.Context, outletID uuid.UUID, key string) (*models.OutletSettingResponse, error) + GetPrinterSettings(ctx context.Context, outletID uuid.UUID) (*models.OutletPrinterSettings, error) + UpdatePrinterSettings(ctx context.Context, outletID uuid.UUID, req *models.UpdateOutletPrinterSettingsRequest) (*models.OutletPrinterSettings, error) + DeleteSetting(ctx context.Context, outletID uuid.UUID, key string) error +} + +type OutletSettingServiceImpl struct { + outletSettingProcessor *processor.OutletSettingProcessorImpl +} + +func NewOutletSettingService(outletSettingProcessor *processor.OutletSettingProcessorImpl) OutletSettingService { + return &OutletSettingServiceImpl{ + outletSettingProcessor: outletSettingProcessor, + } +} + +func (s *OutletSettingServiceImpl) CreateSetting(ctx context.Context, req *models.CreateOutletSettingRequest) (*models.OutletSettingResponse, error) { + return s.outletSettingProcessor.CreateSetting(ctx, req) +} + +func (s *OutletSettingServiceImpl) UpdateSetting(ctx context.Context, outletID uuid.UUID, key string, req *models.UpdateOutletSettingRequest) (*models.OutletSettingResponse, error) { + return s.outletSettingProcessor.UpdateSetting(ctx, outletID, key, req) +} + +func (s *OutletSettingServiceImpl) GetSetting(ctx context.Context, outletID uuid.UUID, key string) (*models.OutletSettingResponse, error) { + return s.outletSettingProcessor.GetSetting(ctx, outletID, key) +} + +func (s *OutletSettingServiceImpl) GetPrinterSettings(ctx context.Context, outletID uuid.UUID) (*models.OutletPrinterSettings, error) { + return s.outletSettingProcessor.GetPrinterSettings(ctx, outletID) +} + +func (s *OutletSettingServiceImpl) UpdatePrinterSettings(ctx context.Context, outletID uuid.UUID, req *models.UpdateOutletPrinterSettingsRequest) (*models.OutletPrinterSettings, error) { + return s.outletSettingProcessor.UpdatePrinterSettings(ctx, outletID, req) +} + +func (s *OutletSettingServiceImpl) DeleteSetting(ctx context.Context, outletID uuid.UUID, key string) error { + return s.outletSettingProcessor.DeleteSetting(ctx, outletID, key) +} diff --git a/internal/service/payment_method_service.go b/internal/service/payment_method_service.go new file mode 100644 index 0000000..8df2366 --- /dev/null +++ b/internal/service/payment_method_service.go @@ -0,0 +1,130 @@ +package service + +import ( + "context" + + "apskel-pos-be/internal/appcontext" + "apskel-pos-be/internal/contract" + "apskel-pos-be/internal/mappers" + "apskel-pos-be/internal/processor" + + "github.com/google/uuid" +) + +type PaymentMethodService interface { + CreatePaymentMethod(ctx context.Context, contextInfo *appcontext.ContextInfo, req *contract.CreatePaymentMethodRequest) *contract.Response + GetPaymentMethodByID(ctx context.Context, id uuid.UUID) *contract.Response + ListPaymentMethods(ctx context.Context, req *contract.ListPaymentMethodsRequest) *contract.Response + UpdatePaymentMethod(ctx context.Context, id uuid.UUID, req *contract.UpdatePaymentMethodRequest) *contract.Response + DeletePaymentMethod(ctx context.Context, id uuid.UUID) *contract.Response + GetActivePaymentMethodsByOrganization(ctx context.Context, organizationID uuid.UUID) *contract.Response +} + +type PaymentMethodServiceImpl struct { + paymentMethodProcessor processor.PaymentMethodProcessor +} + +func NewPaymentMethodService(paymentMethodProcessor processor.PaymentMethodProcessor) PaymentMethodService { + return &PaymentMethodServiceImpl{ + paymentMethodProcessor: paymentMethodProcessor, + } +} + +func (s *PaymentMethodServiceImpl) CreatePaymentMethod(ctx context.Context, contextInfo *appcontext.ContextInfo, req *contract.CreatePaymentMethodRequest) *contract.Response { + // Convert contract to model + modelReq := mappers.CreatePaymentMethodContractToModel(req) + + // Set organization ID from context if not provided + if modelReq.OrganizationID == uuid.Nil && contextInfo != nil { + modelReq.OrganizationID = contextInfo.OrganizationID + } + + // Process request + response, err := s.paymentMethodProcessor.CreatePaymentMethod(ctx, modelReq) + if err != nil { + return contract.BuildErrorResponse([]*contract.ResponseError{ + contract.NewResponseError("PAYMENT_METHOD_CREATE_ERROR", "payment_method", err.Error()), + }) + } + + // Convert model to contract + contractResponse := mappers.PaymentMethodResponseToContract(response) + return contract.BuildSuccessResponse(contractResponse) +} + +func (s *PaymentMethodServiceImpl) GetPaymentMethodByID(ctx context.Context, id uuid.UUID) *contract.Response { + response, err := s.paymentMethodProcessor.GetPaymentMethodByID(ctx, id) + if err != nil { + return contract.BuildErrorResponse([]*contract.ResponseError{ + contract.NewResponseError("PAYMENT_METHOD_NOT_FOUND", "payment_method", err.Error()), + }) + } + + contractResponse := mappers.PaymentMethodResponseToContract(response) + return contract.BuildSuccessResponse(contractResponse) +} + +func (s *PaymentMethodServiceImpl) ListPaymentMethods(ctx context.Context, req *contract.ListPaymentMethodsRequest) *contract.Response { + // Convert contract to model + modelReq := mappers.ListPaymentMethodsContractToModel(req) + + // Process request + response, err := s.paymentMethodProcessor.ListPaymentMethods(ctx, modelReq) + if err != nil { + return contract.BuildErrorResponse([]*contract.ResponseError{ + contract.NewResponseError("PAYMENT_METHOD_LIST_ERROR", "payment_method", err.Error()), + }) + } + + // Convert model to contract + contractResponse := mappers.ListPaymentMethodsResponseToContract(response) + return contract.BuildSuccessResponse(contractResponse) +} + +func (s *PaymentMethodServiceImpl) UpdatePaymentMethod(ctx context.Context, id uuid.UUID, req *contract.UpdatePaymentMethodRequest) *contract.Response { + // Convert contract to model + modelReq := mappers.UpdatePaymentMethodContractToModel(req) + + // Process request + response, err := s.paymentMethodProcessor.UpdatePaymentMethod(ctx, id, modelReq) + if err != nil { + return contract.BuildErrorResponse([]*contract.ResponseError{ + contract.NewResponseError("PAYMENT_METHOD_UPDATE_ERROR", "payment_method", err.Error()), + }) + } + + // Convert model to contract + contractResponse := mappers.PaymentMethodResponseToContract(response) + return contract.BuildSuccessResponse(contractResponse) +} + +func (s *PaymentMethodServiceImpl) DeletePaymentMethod(ctx context.Context, id uuid.UUID) *contract.Response { + err := s.paymentMethodProcessor.DeletePaymentMethod(ctx, id) + if err != nil { + return contract.BuildErrorResponse([]*contract.ResponseError{ + contract.NewResponseError("PAYMENT_METHOD_DELETE_ERROR", "payment_method", err.Error()), + }) + } + + return contract.BuildSuccessResponse(map[string]string{"message": "Payment method deleted successfully"}) +} + +func (s *PaymentMethodServiceImpl) GetActivePaymentMethodsByOrganization(ctx context.Context, organizationID uuid.UUID) *contract.Response { + responses, err := s.paymentMethodProcessor.GetActivePaymentMethodsByOrganization(ctx, organizationID) + if err != nil { + return contract.BuildErrorResponse([]*contract.ResponseError{ + contract.NewResponseError("PAYMENT_METHOD_LIST_ERROR", "payment_method", err.Error()), + }) + } + + // Convert models to contracts + contractResponses := make([]contract.PaymentMethodResponse, len(responses)) + for i, response := range responses { + contractResponse := mappers.PaymentMethodResponseToContract(&response) + if contractResponse != nil { + contractResponses[i] = *contractResponse + } + } + + return contract.BuildSuccessResponse(contractResponses) +} diff --git a/internal/service/product_service.go b/internal/service/product_service.go new file mode 100644 index 0000000..61eed11 --- /dev/null +++ b/internal/service/product_service.go @@ -0,0 +1,131 @@ +package service + +import ( + "context" + + "apskel-pos-be/internal/appcontext" + "apskel-pos-be/internal/constants" + "apskel-pos-be/internal/contract" + "apskel-pos-be/internal/processor" + "apskel-pos-be/internal/transformer" + + "github.com/google/uuid" +) + +type ProductService interface { + CreateProduct(ctx context.Context, apctx *appcontext.ContextInfo, req *contract.CreateProductRequest) *contract.Response + UpdateProduct(ctx context.Context, id uuid.UUID, req *contract.UpdateProductRequest) *contract.Response + DeleteProduct(ctx context.Context, id uuid.UUID) *contract.Response + GetProductByID(ctx context.Context, id uuid.UUID) *contract.Response + ListProducts(ctx context.Context, req *contract.ListProductsRequest) *contract.Response +} + +type ProductServiceImpl struct { + productProcessor processor.ProductProcessor +} + +func NewProductService(productProcessor processor.ProductProcessor) *ProductServiceImpl { + return &ProductServiceImpl{ + productProcessor: productProcessor, + } +} + +func (s *ProductServiceImpl) CreateProduct(ctx context.Context, apctx *appcontext.ContextInfo, req *contract.CreateProductRequest) *contract.Response { + modelReq := transformer.CreateProductRequestToModel(apctx, req) + + productResponse, err := s.productProcessor.CreateProduct(ctx, modelReq) + if err != nil { + errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.ProductServiceEntity, err.Error()) + return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) + } + + contractResponse := transformer.ProductModelResponseToResponse(productResponse) + return contract.BuildSuccessResponse(contractResponse) +} + +func (s *ProductServiceImpl) UpdateProduct(ctx context.Context, id uuid.UUID, req *contract.UpdateProductRequest) *contract.Response { + modelReq := transformer.UpdateProductRequestToModel(req) + + productResponse, err := s.productProcessor.UpdateProduct(ctx, id, modelReq) + if err != nil { + errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.ProductServiceEntity, err.Error()) + return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) + } + + contractResponse := transformer.ProductModelResponseToResponse(productResponse) + return contract.BuildSuccessResponse(contractResponse) +} + +func (s *ProductServiceImpl) DeleteProduct(ctx context.Context, id uuid.UUID) *contract.Response { + err := s.productProcessor.DeleteProduct(ctx, id) + if err != nil { + errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.ProductServiceEntity, err.Error()) + return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) + } + + return contract.BuildSuccessResponse(map[string]interface{}{ + "message": "Product deleted successfully", + }) +} + +func (s *ProductServiceImpl) GetProductByID(ctx context.Context, id uuid.UUID) *contract.Response { + productResponse, err := s.productProcessor.GetProductByID(ctx, id) + if err != nil { + errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.ProductServiceEntity, err.Error()) + return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) + } + + contractResponse := transformer.ProductModelResponseToResponse(productResponse) + return contract.BuildSuccessResponse(contractResponse) +} + +func (s *ProductServiceImpl) ListProducts(ctx context.Context, req *contract.ListProductsRequest) *contract.Response { + // Build filters + filters := make(map[string]interface{}) + if req.OrganizationID != nil { + filters["organization_id"] = *req.OrganizationID + } + if req.CategoryID != nil { + filters["category_id"] = *req.CategoryID + } + if req.BusinessType != "" { + filters["business_type"] = req.BusinessType + } + if req.IsActive != nil { + filters["is_active"] = *req.IsActive + } + if req.Search != "" { + filters["search"] = req.Search + } + if req.MinPrice != nil { + filters["price_min"] = *req.MinPrice + } + if req.MaxPrice != nil { + filters["price_max"] = *req.MaxPrice + } + + products, totalCount, err := s.productProcessor.ListProducts(ctx, filters, req.Page, req.Limit) + if err != nil { + errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.ProductServiceEntity, err.Error()) + return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) + } + + // Convert to contract responses + contractResponses := transformer.ProductsToResponses(products) + + // Calculate total pages + totalPages := totalCount / req.Limit + if totalCount%req.Limit > 0 { + totalPages++ + } + + listResponse := &contract.ListProductsResponse{ + Products: contractResponses, + TotalCount: totalCount, + Page: req.Page, + Limit: req.Limit, + TotalPages: totalPages, + } + + return contract.BuildSuccessResponse(listResponse) +} diff --git a/internal/service/product_variant_service.go b/internal/service/product_variant_service.go new file mode 100644 index 0000000..d395e61 --- /dev/null +++ b/internal/service/product_variant_service.go @@ -0,0 +1,91 @@ +package service + +import ( + "context" + + "apskel-pos-be/internal/appcontext" + "apskel-pos-be/internal/constants" + "apskel-pos-be/internal/contract" + "apskel-pos-be/internal/processor" + "apskel-pos-be/internal/transformer" + + "github.com/google/uuid" +) + +type ProductVariantService interface { + CreateProductVariant(ctx context.Context, apctx *appcontext.ContextInfo, req *contract.CreateProductVariantRequest) *contract.Response + UpdateProductVariant(ctx context.Context, id uuid.UUID, req *contract.UpdateProductVariantRequest) *contract.Response + DeleteProductVariant(ctx context.Context, id uuid.UUID) *contract.Response + GetProductVariantByID(ctx context.Context, id uuid.UUID) *contract.Response + GetProductVariantsByProductID(ctx context.Context, productID uuid.UUID) *contract.Response +} + +type ProductVariantServiceImpl struct { + productVariantProcessor processor.ProductVariantProcessor +} + +func NewProductVariantService(productVariantProcessor processor.ProductVariantProcessor) *ProductVariantServiceImpl { + return &ProductVariantServiceImpl{ + productVariantProcessor: productVariantProcessor, + } +} + +func (s *ProductVariantServiceImpl) CreateProductVariant(ctx context.Context, apctx *appcontext.ContextInfo, req *contract.CreateProductVariantRequest) *contract.Response { + modelReq := transformer.CreateProductVariantRequestToModel(req) + + variantResponse, err := s.productVariantProcessor.CreateProductVariant(ctx, modelReq) + if err != nil { + errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.ProductVariantServiceEntity, err.Error()) + return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) + } + + contractResponse := transformer.ProductVariantModelResponseToResponse(variantResponse) + return contract.BuildSuccessResponse(contractResponse) +} + +func (s *ProductVariantServiceImpl) UpdateProductVariant(ctx context.Context, id uuid.UUID, req *contract.UpdateProductVariantRequest) *contract.Response { + modelReq := transformer.UpdateProductVariantRequestToModel(req) + + variantResponse, err := s.productVariantProcessor.UpdateProductVariant(ctx, id, modelReq) + if err != nil { + errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.ProductVariantServiceEntity, err.Error()) + return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) + } + + contractResponse := transformer.ProductVariantModelResponseToResponse(variantResponse) + return contract.BuildSuccessResponse(contractResponse) +} + +func (s *ProductVariantServiceImpl) DeleteProductVariant(ctx context.Context, id uuid.UUID) *contract.Response { + err := s.productVariantProcessor.DeleteProductVariant(ctx, id) + if err != nil { + errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.ProductVariantServiceEntity, err.Error()) + return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) + } + + return contract.BuildSuccessResponse(map[string]interface{}{ + "message": "Product variant deleted successfully", + }) +} + +func (s *ProductVariantServiceImpl) GetProductVariantByID(ctx context.Context, id uuid.UUID) *contract.Response { + variantResponse, err := s.productVariantProcessor.GetProductVariantByID(ctx, id) + if err != nil { + errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.ProductVariantServiceEntity, err.Error()) + return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) + } + + contractResponse := transformer.ProductVariantModelResponseToResponse(variantResponse) + return contract.BuildSuccessResponse(contractResponse) +} + +func (s *ProductVariantServiceImpl) GetProductVariantsByProductID(ctx context.Context, productID uuid.UUID) *contract.Response { + variants, err := s.productVariantProcessor.GetProductVariantsByProductID(ctx, productID) + if err != nil { + errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.ProductVariantServiceEntity, err.Error()) + return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) + } + + contractResponses := transformer.ProductVariantsToResponses(variants) + return contract.BuildSuccessResponse(contractResponses) +} diff --git a/internal/service/user_processor.go b/internal/service/user_processor.go new file mode 100644 index 0000000..464a2fd --- /dev/null +++ b/internal/service/user_processor.go @@ -0,0 +1,22 @@ +package service + +import ( + "apskel-pos-be/internal/entities" + "apskel-pos-be/internal/models" + "context" + + "github.com/google/uuid" +) + +type UserProcessor interface { + UpdateUser(ctx context.Context, id uuid.UUID, req *models.UpdateUserRequest) (*models.UserResponse, error) + CreateUser(ctx context.Context, req *models.CreateUserRequest) (*models.UserResponse, error) + DeleteUser(ctx context.Context, id uuid.UUID) error + GetUserByID(ctx context.Context, id uuid.UUID) (*models.UserResponse, error) + GetUserByEmail(ctx context.Context, email string) (*models.UserResponse, error) + ListUsers(ctx context.Context, organizationID uuid.UUID, page, limit int) ([]models.UserResponse, int, error) + GetUserEntityByEmail(ctx context.Context, email string) (*entities.User, error) + ChangePassword(ctx context.Context, userID uuid.UUID, req *models.ChangePasswordRequest) error + ActivateUser(ctx context.Context, userID uuid.UUID) error + DeactivateUser(ctx context.Context, userID uuid.UUID) error +} diff --git a/internal/service/user_service.go b/internal/service/user_service.go new file mode 100644 index 0000000..334607e --- /dev/null +++ b/internal/service/user_service.go @@ -0,0 +1,101 @@ +package service + +import ( + "context" + + "apskel-pos-be/internal/contract" + "apskel-pos-be/internal/transformer" + + "github.com/google/uuid" +) + +type UserServiceImpl struct { + userProcessor UserProcessor +} + +func NewUserService(userProcessor UserProcessor) *UserServiceImpl { + return &UserServiceImpl{ + userProcessor: userProcessor, + } +} + +func (s *UserServiceImpl) CreateUser(ctx context.Context, req *contract.CreateUserRequest) (*contract.UserResponse, error) { + modelReq := transformer.CreateUserRequestToModel(req) + + userResponse, err := s.userProcessor.CreateUser(ctx, modelReq) + if err != nil { + return nil, err + } + + contractResponse := transformer.UserModelResponseToResponse(userResponse) + return contractResponse, nil +} + +func (s *UserServiceImpl) UpdateUser(ctx context.Context, id uuid.UUID, req *contract.UpdateUserRequest) (*contract.UserResponse, error) { + modelReq := transformer.UpdateUserRequestToModel(req) + + userResponse, err := s.userProcessor.UpdateUser(ctx, id, modelReq) + if err != nil { + return nil, err + } + + contractResponse := transformer.UserModelResponseToResponse(userResponse) + + return contractResponse, nil +} + +func (s *UserServiceImpl) DeleteUser(ctx context.Context, id uuid.UUID) error { + return s.userProcessor.DeleteUser(ctx, id) +} + +func (s *UserServiceImpl) GetUserByID(ctx context.Context, id uuid.UUID) (*contract.UserResponse, error) { + userResponse, err := s.userProcessor.GetUserByID(ctx, id) + if err != nil { + return nil, err + } + + contractResponse := transformer.UserModelResponseToResponse(userResponse) + + return contractResponse, nil +} + +func (s *UserServiceImpl) GetUserByEmail(ctx context.Context, email string) (*contract.UserResponse, error) { + userResponse, err := s.userProcessor.GetUserByEmail(ctx, email) + if err != nil { + return nil, err + } + + contractResponse := transformer.UserModelResponseToResponse(userResponse) + + return contractResponse, nil +} + +func (s *UserServiceImpl) ListUsers(ctx context.Context, req *contract.ListUsersRequest) (*contract.ListUsersResponse, error) { + page, limit := transformer.PaginationToRequest(req.Page, req.Limit) + + organizationID := uuid.New() + userResponses, totalCount, err := s.userProcessor.ListUsers(ctx, organizationID, page, limit) + if err != nil { + return nil, err + } + + contractResponses := transformer.UserResponsesToResponses(userResponses) + + response := transformer.CreateListUsersResponse(contractResponses, totalCount, page, limit) + + return response, nil +} + +func (s *UserServiceImpl) ChangePassword(ctx context.Context, userID uuid.UUID, req *contract.ChangePasswordRequest) error { + modelReq := transformer.ChangePasswordRequestToModel(req) + + return s.userProcessor.ChangePassword(ctx, userID, modelReq) +} + +func (s *UserServiceImpl) ActivateUser(ctx context.Context, userID uuid.UUID) error { + return s.userProcessor.ActivateUser(ctx, userID) +} + +func (s *UserServiceImpl) DeactivateUser(ctx context.Context, userID uuid.UUID) error { + return s.userProcessor.DeactivateUser(ctx, userID) +} diff --git a/internal/services/.DS_Store b/internal/services/.DS_Store deleted file mode 100644 index 75e88fa4955076bf892a698ec4b7db127d79c32e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKOHRWu5PhaesbJG3OJ9K-L{&Kf7l0;}K&qN5Nm=LUyzvYuE?HLdX8+J{hymlP#Hj zpm#%KJ6)xuvX}h1|E<0U-AZK8q)3x0HFZ^U?i~KyqXU>2#@Xr_s#rAc(;X~Qo`seZFu8nMOY$D<} p%7DUg_7lK~oFh-ksP-sh`c;ObqGS<$3McxHKqACDXW$A9d;_piMZo|7 diff --git a/internal/services/auth/init.go b/internal/services/auth/init.go deleted file mode 100644 index d8859a4..0000000 --- a/internal/services/auth/init.go +++ /dev/null @@ -1,202 +0,0 @@ -package auth - -import ( - "context" - "enaklo-pos-be/config" - "enaklo-pos-be/internal/common/mycontext" - "enaklo-pos-be/internal/entity" - "enaklo-pos-be/internal/utils" - "fmt" - "go.uber.org/zap" - - "enaklo-pos-be/internal/common/errors" - "enaklo-pos-be/internal/common/logger" - "enaklo-pos-be/internal/repository" -) - -type AuthServiceImpl struct { - authRepo repository.Auth - crypto repository.Crypto - user repository.User - emailSvc repository.EmailService - emailCfg config.Email - trxRepo repository.Trx - license repository.License -} - -func New(authRepo repository.Auth, - crypto repository.Crypto, user repository.User, emailSvc repository.EmailService, - emailCfg config.Email, trxRepo repository.Trx, - license repository.License, -) *AuthServiceImpl { - return &AuthServiceImpl{ - authRepo: authRepo, - crypto: crypto, - user: user, - emailSvc: emailSvc, - emailCfg: emailCfg, - trxRepo: trxRepo, - license: license, - } -} - -func (u *AuthServiceImpl) AuthenticateUser(ctx context.Context, email, password string) (*entity.AuthenticateUser, error) { - user, err := u.authRepo.CheckExistsUserAccount(ctx, email) - if err != nil { - logger.ContextLogger(ctx).Error("error when get user", zap.Error(err)) - return nil, errors.ErrorInternalServer - } - - if user == nil { - return nil, errors.ErrorUserIsNotFound - } - - if ok := u.crypto.CompareHashAndPassword(user.Password, password); !ok { - return nil, errors.ErrorUserInvalidLogin - } - - signedToken, err := u.crypto.GenerateJWT(user.ToUser()) - - if err != nil { - return nil, err - } - var licensePartner entity.PartnerLicense - - if user.PartnerID != nil && *user.PartnerID != 0 { - partnerLicense, err := u.license.FindByPartnerIDMaxEndDate(ctx, user.PartnerID) - if err != nil { - logger.ContextLogger(ctx).Error("error when get user license", zap.Error(err)) - return nil, errors.ErrorInternalServer - } - - if partnerLicense == nil { - return nil, errors.ErrorInvalidLicense - } - - licensePartner = partnerLicense.ToPartnerLicense() - if licensePartner.LicenseStatus == "EXPIRED" || licensePartner.LicenseStatus == "INACTIVE" { - return nil, errors.ErrorInvalidLicense - } - } - - return user.ToUserAuthenticate(signedToken, licensePartner), nil -} - -func (u *AuthServiceImpl) SendPasswordResetLink(ctx context.Context, email string) error { - user, err := u.authRepo.CheckExistsUserAccount(ctx, email) - if err != nil { - logger.ContextLogger(ctx).Error("error when getting user", zap.Error(err)) - return errors.ErrorInternalServer - } - - if user == nil { - return errors.ErrorUserIsNotFound - } - - // Begin a transaction - trx, err := u.trxRepo.Begin(ctx) - if err != nil { - logger.ContextLogger(ctx).Error("error when beginning transaction", zap.Error(err)) - return errors.ErrorInternalServer - } - - defer func() { - if r := recover(); r != nil { - u.trxRepo.Rollback(trx) - logger.ContextLogger(ctx).Error("panic recovered in SendPasswordResetLink", zap.Any("recover", r)) - err = errors.ErrorInternalServer - } - }() - - // Generate a new password - generatedPassword := utils.GenerateRandomString(10) - hashPassword, err := user.ToUser().HashedPassword(generatedPassword) - if err != nil { - logger.ContextLogger(ctx).Error("error when generating hashed password", zap.Error(err)) - u.trxRepo.Rollback(trx) - return errors.ErrorInternalServer - } - - if err := u.authRepo.UpdatePassword(ctx, trx, hashPassword, user.ID, true); err != nil { - logger.ContextLogger(ctx).Error("error when updating user password", zap.Error(err)) - u.trxRepo.Rollback(trx) - return errors.ErrorInternalServer - } - - if u.emailCfg.CustomReceiver != "" { - email = u.emailCfg.CustomReceiver - } - - sender := u.emailCfg.Sender - templatePath := u.emailCfg.ResetPassword.TemplatePath - subject := fmt.Sprintf("enaklo-pos %s", u.emailCfg.ResetPassword.Subject) - if user.UserType == "CUSTOMER" { - sender = u.emailCfg.SenderCustomer - templatePath = u.emailCfg.ResetPassword.TemplatePathCustomer - subject = fmt.Sprintf("Ayogo %s", u.emailCfg.ResetPassword.Subject) - } - - // Prepare the email notification parameters - renewPasswordRequest := entity.SendEmailNotificationParam{ - Sender: sender, - Recipient: email, - Subject: subject, - TemplateName: u.emailCfg.ResetPassword.TemplateName, - TemplatePath: templatePath, - Data: map[string]interface{}{ - "Name": user.Name, - "OpeningWord": u.emailCfg.ResetPassword.OpeningWord, - "Password": generatedPassword, - "ClosingWord": u.emailCfg.ResetPassword.ClosingWord, - "Note": u.emailCfg.ResetPassword.Notes, - "Link": u.emailCfg.ResetPassword.Link, - }, - } - - defer func() { - if r := recover(); r != nil { - u.trxRepo.Rollback(trx) - logger.ContextLogger(ctx).Error("panic recovered in SendPasswordResetLink", zap.Any("recover", r)) - err = errors.ErrorInternalServer - } - }() - - // Send the email notification - err = u.emailSvc.SendEmailTransactional(ctx, renewPasswordRequest) - if err != nil { - u.trxRepo.Rollback(trx) - logger.ContextLogger(ctx).Error("error when sending password reset email", zap.Error(err)) - return errors.ErrorExternalRequest - } - - trx.Commit() - - return nil -} - -func (u *AuthServiceImpl) ResetPassword(ctx mycontext.Context, oldPassword, newPassword string) error { - user, err := u.authRepo.CheckExistsUserAccountByID(ctx, ctx.RequestedBy()) - if err != nil { - return errors.ErrorInvalidRequest - } - - if ok := u.crypto.CompareHashAndPassword(user.Password, oldPassword); !ok { - return errors.ErrorUserInvalidLogin - } - - password, err := user.ToUser().HashedPassword(newPassword) - if err != nil { - return errors.ErrorInvalidRequest - } - - trx, _ := u.trxRepo.Begin(ctx) - - err = u.authRepo.UpdatePassword(ctx, trx, password, user.ID, false) - if err != nil { - return errors.ErrorInternalServer - } - - trx.Commit() - - return nil -} diff --git a/internal/services/balance/balance.go b/internal/services/balance/balance.go deleted file mode 100644 index 3aab24a..0000000 --- a/internal/services/balance/balance.go +++ /dev/null @@ -1,138 +0,0 @@ -package balance - -import ( - "context" - "enaklo-pos-be/internal/common/logger" - "enaklo-pos-be/internal/common/mycontext" - "enaklo-pos-be/internal/entity" - "enaklo-pos-be/internal/repository" - "errors" - "go.uber.org/zap" -) - -type Config interface { - GetPlatformFee() int64 -} - -type BalanceService struct { - repo repository.WalletRepository - trx repository.Trx - crypt repository.Crypto - transaction repository.TransactionRepository - cfg Config -} - -func NewBalanceService(repo repository.WalletRepository, - trx repository.Trx, - crypt repository.Crypto, cfg Config, - transaction repository.TransactionRepository) *BalanceService { - return &BalanceService{ - repo: repo, - trx: trx, - crypt: crypt, - cfg: cfg, - transaction: transaction, - } -} - -func (s *BalanceService) GetByID(ctx context.Context, id int64) (*entity.Balance, error) { - balanceDB, err := s.repo.GetByPartnerID(ctx, nil, id) - if err != nil { - logger.ContextLogger(ctx).Error("error when get branch by id", zap.Error(err)) - return nil, err - } - - return &entity.Balance{ - PartnerID: id, - Balance: balanceDB.Balance, - AuthBalance: balanceDB.AuthBalance, - }, nil -} - -func (s *BalanceService) WithdrawInquiry(ctx context.Context, req *entity.BalanceWithdrawInquiry) (*entity.BalanceWithdrawInquiryResponse, error) { - balanceDB, err := s.repo.GetForUpdate(ctx, nil, req.PartnerID) - if err != nil { - logger.ContextLogger(ctx).Error("error when get branch by id", zap.Error(err)) - return nil, err - } - - if float64(req.Amount) > balanceDB.Balance { - logger.ContextLogger(ctx).Error("requested amount exceeds available balance") - return nil, errors.New("insufficient balance") - } - - token, err := s.crypt.GenerateJWTWithdraw(&entity.WalletWithdrawRequest{ - ID: balanceDB.ID, - PartnerID: req.PartnerID, - Amount: req.Amount - s.cfg.GetPlatformFee(), - Fee: s.cfg.GetPlatformFee(), - Total: req.Amount, - }) - - return &entity.BalanceWithdrawInquiryResponse{ - PartnerID: req.PartnerID, - Amount: req.Amount - s.cfg.GetPlatformFee(), - Token: token, - Fee: s.cfg.GetPlatformFee(), - Total: req.Amount, - }, nil -} - -func (s *BalanceService) WithdrawExecute(ctx mycontext.Context, req *entity.WalletWithdrawRequest) (*entity.WalletWithdrawResponse, error) { - decodedReq, err := s.crypt.ValidateJWTWithdraw(req.Token) - if err != nil || decodedReq.PartnerID != req.PartnerID { - logger.ContextLogger(ctx).Error("invalid withdrawal token", zap.Error(err)) - return nil, errors.New("invalid withdrawal token") - } - - trx, _ := s.trx.Begin(ctx) - wallet, err := s.repo.GetForUpdate(ctx, trx, decodedReq.PartnerID) - if err != nil { - logger.ContextLogger(ctx).Error("error retrieving wallet by partner ID", zap.Error(err)) - trx.Rollback() - return nil, err - } - - totalAmount := float64(decodedReq.Total) - if totalAmount > wallet.Balance { - logger.ContextLogger(ctx).Error("insufficient balance for withdrawal", zap.Float64("available", wallet.Balance), zap.Float64("requested", totalAmount)) - trx.Rollback() - return nil, errors.New("insufficient balance") - } - - wallet.Balance -= totalAmount - wallet.AuthBalance += totalAmount - - if _, err := s.repo.Update(ctx, trx, wallet); err != nil { - logger.ContextLogger(ctx).Error("error updating wallet balance", zap.Error(err)) - trx.Rollback() - return nil, err - } - - transaction := &entity.Transaction{ - PartnerID: wallet.PartnerID, - TransactionType: "WITHDRAW", - Status: "WAITING_APPROVAL", - CreatedBy: ctx.RequestedBy(), - Amount: totalAmount, - } - - transaction, err = s.transaction.Create(ctx, trx, transaction) - if err != nil { - logger.ContextLogger(ctx).Error("error creating transaction record", zap.Error(err)) - trx.Rollback() - return nil, err - } - - if err := trx.Commit().Error; err != nil { - logger.ContextLogger(ctx).Error("error committing transaction", zap.Error(err)) - return nil, err - } - - response := &entity.WalletWithdrawResponse{ - TransactionID: transaction.ID, - Status: "WAITING_APPROVAL", - } - - return response, nil -} diff --git a/internal/services/license/license.go b/internal/services/license/license.go deleted file mode 100644 index 680c9ca..0000000 --- a/internal/services/license/license.go +++ /dev/null @@ -1,71 +0,0 @@ -package service - -import ( - "context" - "enaklo-pos-be/internal/common/logger" - "enaklo-pos-be/internal/common/mycontext" - "enaklo-pos-be/internal/entity" - "enaklo-pos-be/internal/repository" - - "go.uber.org/zap" -) - -type LicenseService struct { - repo repository.License -} - -func NewLicenseService(repo repository.License) *LicenseService { - return &LicenseService{ - repo: repo, - } -} - -func (s *LicenseService) Create(ctx mycontext.Context, licenseReq *entity.License) (*entity.License, error) { - licenseDB := licenseReq.ToLicenseDB() - licenseDB.CreatedBy = ctx.RequestedBy() - licenseDB.UpdatedBy = ctx.RequestedBy() - - licenseDB, err := s.repo.Create(ctx, licenseDB) - if err != nil { - logger.ContextLogger(ctx).Error("error when create license", zap.Error(err)) - return nil, err - } - - return licenseDB.ToLicense(), nil -} - -func (s *LicenseService) Update(ctx mycontext.Context, id string, licenseReq *entity.License) (*entity.License, error) { - existingLicense, err := s.repo.FindByID(ctx, id) - if err != nil { - return nil, err - } - - existingLicense.ToUpdatedLicense(ctx.RequestedBy(), *licenseReq) - - updatedLicenseDB, err := s.repo.Update(ctx, existingLicense) - if err != nil { - logger.ContextLogger(ctx).Error("error when update license", zap.Error(err)) - return nil, err - } - - return updatedLicenseDB.ToLicense(), nil -} - -func (s *LicenseService) GetByID(ctx context.Context, id string) (*entity.License, error) { - licenseDB, err := s.repo.FindByID(ctx, id) - if err != nil { - logger.ContextLogger(ctx).Error("error when get license by id", zap.Error(err)) - return nil, err - } - - return licenseDB.ToLicense(), nil -} - -func (s *LicenseService) GetAll(ctx context.Context, limit, offset int, status string) ([]*entity.LicenseGetAll, int64, error) { - licenses, total, err := s.repo.GetAll(ctx, limit, offset, status) - if err != nil { - logger.ContextLogger(ctx).Error("error when getting all licenses", zap.Error(err)) - return nil, 0, err - } - return licenses, total, nil -} diff --git a/internal/services/member/member.go b/internal/services/member/member.go deleted file mode 100644 index c43ffe9..0000000 --- a/internal/services/member/member.go +++ /dev/null @@ -1,58 +0,0 @@ -package member - -import ( - "context" - "enaklo-pos-be/internal/common/mycontext" - "enaklo-pos-be/internal/constants" - "enaklo-pos-be/internal/entity" - "time" -) - -type RegistrationService interface { - InitiateRegistration(ctx mycontext.Context, request *entity.MemberRegistrationRequest) (*entity.MemberRegistrationResponse, error) - VerifyOTP(ctx mycontext.Context, token string, otp string) (*entity.MemberVerificationResponse, error) - GetRegistrationStatus(ctx mycontext.Context, token string) (*entity.MemberRegistrationStatus, error) - ResendOTP(ctx mycontext.Context, token string) (*entity.ResendOTPResponse, error) -} - -type CryptoSvc interface { - GenerateJWTCustomer(user *entity.Customer) (string, error) -} - -type memberSvc struct { - repo Repository - notification NotificationService - customerSvc CustomerService - crypt CryptoSvc -} - -type Repository interface { - CreateRegistration(ctx mycontext.Context, registration *entity.MemberRegistration) (*entity.MemberRegistration, error) - GetRegistrationByToken(ctx mycontext.Context, token string) (*entity.MemberRegistration, error) - UpdateRegistrationStatus(ctx mycontext.Context, token string, status constants.RegistrationStatus) error - UpdateRegistrationOTP(ctx mycontext.Context, token string, otp string, expiresAt time.Time) error -} - -type NotificationService interface { - SendEmailTransactional(ctx context.Context, param entity.SendEmailNotificationParam) error -} - -type CustomerService interface { - ResolveCustomer(ctx mycontext.Context, req *entity.CustomerResolutionRequest) (int64, error) - GetCustomer(ctx mycontext.Context, id int64) (*entity.Customer, error) - CustomerCheck(ctx mycontext.Context, req *entity.CustomerResolutionRequest) (*entity.CustomerCheckResponse, error) -} - -func NewMemberRegistrationService( - repo Repository, - notification NotificationService, - customerSvc CustomerService, - crypt CryptoSvc, -) RegistrationService { - return &memberSvc{ - repo: repo, - notification: notification, - customerSvc: customerSvc, - crypt: crypt, - } -} diff --git a/internal/services/member/member_registration.go b/internal/services/member/member_registration.go deleted file mode 100644 index bc439e2..0000000 --- a/internal/services/member/member_registration.go +++ /dev/null @@ -1,270 +0,0 @@ -package member - -import ( - "enaklo-pos-be/internal/common/logger" - "enaklo-pos-be/internal/common/mycontext" - "enaklo-pos-be/internal/constants" - "enaklo-pos-be/internal/entity" - "errors" - "go.uber.org/zap" - "golang.org/x/exp/rand" - "time" -) - -func (s *memberSvc) InitiateRegistration( - ctx mycontext.Context, - request *entity.MemberRegistrationRequest, -) (*entity.MemberRegistrationResponse, error) { - customerResolution := &entity.CustomerResolutionRequest{ - Email: request.Email, - PhoneNumber: request.Phone, - } - - checkResult, err := s.customerSvc.CustomerCheck(ctx, customerResolution) - if checkResult.Exists { - return nil, errors.New(checkResult.Message) - } - - otp := generateOTP(6) - - token := constants.GenerateUUID() - - registration := &entity.MemberRegistration{ - ID: constants.GenerateUUID(), - Token: token, - Name: request.Name, - Email: request.Email, - Phone: request.Phone, - BirthDate: request.BirthDate, - OTP: otp, - Status: constants.RegistrationPending, - ExpiresAt: constants.TimeNow().Add(10 * time.Minute), - CreatedAt: constants.TimeNow(), - UpdatedAt: constants.TimeNow(), - BranchID: request.BranchID, - CashierID: request.CashierID, - Password: request.GetHashPassword(), - } - - savedRegistration, err := s.repo.CreateRegistration(ctx, registration) - if err != nil { - logger.ContextLogger(ctx).Error("failed to create member registration", zap.Error(err)) - return nil, err - } - - err = s.sendRegistrationOTP(ctx, savedRegistration) - if err != nil { - logger.ContextLogger(ctx).Warn("failed to send OTP", zap.Error(err)) - } - - return &entity.MemberRegistrationResponse{ - Token: token, - Status: savedRegistration.Status.String(), - ExpiresAt: savedRegistration.ExpiresAt, - Message: "Registration initiated. Please verify with OTP sent to your email.", - }, nil -} - -func (s *memberSvc) VerifyOTP( - ctx mycontext.Context, - token string, - otp string, -) (*entity.MemberVerificationResponse, error) { - logger.ContextLogger(ctx).Info("verifying OTP for member registration", zap.String("token", token)) - - registration, err := s.repo.GetRegistrationByToken(ctx, token) - if err != nil { - logger.ContextLogger(ctx).Error("failed to get registration", zap.Error(err)) - return nil, errors.New("invalid registration token") - } - - if registration.Status == constants.RegistrationSuccess { - return nil, errors.New("registration already completed") - } - - if registration.ExpiresAt.Before(constants.TimeNow()) { - return nil, errors.New("registration expired") - } - - if registration.OTP != otp { - return nil, errors.New("invalid OTP") - } - - customerResolution := &entity.CustomerResolutionRequest{ - Name: registration.Name, - Email: registration.Email, - PhoneNumber: registration.Phone, - BirthDate: registration.BirthDate, - Password: registration.Password, - } - - customerID, err := s.customerSvc.ResolveCustomer(ctx, customerResolution) - if err != nil { - logger.ContextLogger(ctx).Error("failed to create customer", zap.Error(err)) - return nil, errors.New("failed to create member record") - } - - err = s.repo.UpdateRegistrationStatus(ctx, token, constants.RegistrationSuccess) - if err != nil { - logger.ContextLogger(ctx).Warn("failed to update registration status", zap.Error(err)) - } - - customer, err := s.customerSvc.GetCustomer(ctx, customerID) - if err != nil { - logger.ContextLogger(ctx).Warn("failed to get created customer", zap.Error(err)) - - return &entity.MemberVerificationResponse{ - CustomerID: customerID, - Name: registration.Name, - Email: registration.Email, - Phone: registration.Phone, - Status: "Registration completed successfully", - }, nil - } - - err = s.sendWelcomeEmail(ctx, customer) - if err != nil { - logger.ContextLogger(ctx).Warn("failed to send welcome email", zap.Error(err)) - } - - signedToken, err := s.crypt.GenerateJWTCustomer(customer) - - if err != nil { - return nil, err - } - - return &entity.MemberVerificationResponse{ - Auth: customer.ToUserAuthenticate(signedToken), - CustomerID: customer.ID, - Name: customer.Name, - Email: customer.Email, - Phone: customer.Phone, - Points: customer.Points, - Status: "Registration completed successfully", - }, nil -} - -func (s *memberSvc) GetRegistrationStatus( - ctx mycontext.Context, - token string, -) (*entity.MemberRegistrationStatus, error) { - logger.ContextLogger(ctx).Info("checking registration status", zap.String("token", token)) - - registration, err := s.repo.GetRegistrationByToken(ctx, token) - if err != nil { - logger.ContextLogger(ctx).Error("failed to get registration", zap.Error(err)) - return nil, errors.New("invalid registration token") - } - - return &entity.MemberRegistrationStatus{ - Token: registration.Token, - Status: registration.Status.String(), - ExpiresAt: registration.ExpiresAt, - IsExpired: registration.ExpiresAt.Before(constants.TimeNow()), - CreatedAt: registration.CreatedAt, - }, nil -} - -func (s *memberSvc) ResendOTP( - ctx mycontext.Context, - token string, -) (*entity.ResendOTPResponse, error) { - logger.ContextLogger(ctx).Info("resending OTP", zap.String("token", token)) - - registration, err := s.repo.GetRegistrationByToken(ctx, token) - if err != nil { - logger.ContextLogger(ctx).Error("failed to get registration", zap.Error(err)) - return nil, errors.New("invalid registration token") - } - - if registration.Status == constants.RegistrationSuccess { - return nil, errors.New("registration already completed") - } - - newOTP := generateOTP(6) - newExpiresAt := constants.TimeNow().Add(10 * time.Minute) - - err = s.repo.UpdateRegistrationOTP(ctx, token, newOTP, newExpiresAt) - if err != nil { - logger.ContextLogger(ctx).Error("failed to update OTP", zap.Error(err)) - return nil, errors.New("failed to generate new OTP") - } - - registration.OTP = newOTP - registration.ExpiresAt = newExpiresAt - - err = s.sendRegistrationOTP(ctx, registration) - if err != nil { - logger.ContextLogger(ctx).Warn("failed to send OTP", zap.Error(err)) - } - - return &entity.ResendOTPResponse{ - Token: token, - ExpiresAt: newExpiresAt, - Message: "OTP has been resent to your email and phone", - }, nil -} - -func (s *memberSvc) sendRegistrationOTP( - ctx mycontext.Context, - registration *entity.MemberRegistration, -) error { - emailData := map[string]interface{}{ - "UserName": registration.Name, - "OTPCode": registration.OTP, - } - - err := s.notification.SendEmailTransactional(ctx, entity.SendEmailNotificationParam{ - Sender: "noreply@enaklo.co.id", - Recipient: registration.Email, - Subject: "Enaklo - Registration Verification Code", - TemplateName: "member_registration_otp", - TemplatePath: "templates/member_registration_otp.html", - Data: emailData, - }) - - if err != nil { - return err - } - - //if registration.Phone != "" { - // smsMessage := fmt.Sprintf("Your Enaklo registration code is: %s. Please provide this code to our staff to complete your registration.", registration.OTP) - // _ = s.notification.SendSMS(ctx, registration.Phone, smsMessage) - //} - - return nil -} - -func (s *memberSvc) sendWelcomeEmail( - ctx mycontext.Context, - customer *entity.Customer, -) error { - - welcomeData := map[string]interface{}{ - "UserName": customer.Name, - "MemberID": customer.CustomerID, - "PointsName": "ELP", - "PointsBalance": customer.Points, - "RedeemLink": "https://enaklo.co.id/redeem", - "CurrentDate": time.Now().Format("01-2006"), - } - - return s.notification.SendEmailTransactional(ctx, entity.SendEmailNotificationParam{ - Sender: "noreply@enaklo.co.id", - Recipient: customer.Email, - Subject: "Welcome to Enaklo Membership Program", - TemplateName: "welcome_member", - TemplatePath: "/templates/welcome_member.html", - Data: welcomeData, - }) -} - -func generateOTP(length int) string { - rand.Seed(uint64(time.Now().Nanosecond())) - digits := "0123456789" - otp := "" - for i := 0; i < length; i++ { - otp += string(digits[rand.Intn(len(digits))]) - } - return otp -} diff --git a/internal/services/oss/impl.go b/internal/services/oss/impl.go deleted file mode 100644 index 91790b1..0000000 --- a/internal/services/oss/impl.go +++ /dev/null @@ -1,45 +0,0 @@ -package oss - -import ( - "context" - "enaklo-pos-be/internal/entity" - "enaklo-pos-be/internal/utils/generator" - "fmt" - "path" - - "github.com/go-playground/validator/v10" -) - -func (s *OssService) UploadFile(ctx context.Context, req *entity.UploadFileRequest) (*entity.UploadFileResponse, error) { - file := req.FileHeader - req.FileSize = file.Size - req.Ext = path.Ext(file.Filename) - validate := validator.New() - if err := validate.Struct(req); err != nil { - return nil, err - } - - // Open the file and read its content - srcFile, err := file.Open() - if err != nil { - return nil, err - } - defer srcFile.Close() - - fileContent := make([]byte, file.Size) - _, err = srcFile.Read(fileContent) - if err != nil { - return nil, err - } - - filePath := fmt.Sprintf("%v/%v%v", req.FolderName, generator.GenerateFileName(), req.Ext) - fileUrl, err := s.ossRepo.UploadFile(ctx, filePath, fileContent) - if err != nil { - return nil, err - } - - return &entity.UploadFileResponse{ - FilePath: filePath, - FileUrl: fileUrl, - }, nil -} diff --git a/internal/services/oss/init.go b/internal/services/oss/init.go deleted file mode 100644 index bd99652..0000000 --- a/internal/services/oss/init.go +++ /dev/null @@ -1,13 +0,0 @@ -package oss - -import "enaklo-pos-be/internal/repository" - -type OssService struct { - ossRepo repository.OSSRepository -} - -func NewOSSService(ossRepo repository.OSSRepository) *OssService { - return &OssService{ - ossRepo: ossRepo, - } -} diff --git a/internal/services/partner/partner.go b/internal/services/partner/partner.go deleted file mode 100644 index e610c4e..0000000 --- a/internal/services/partner/partner.go +++ /dev/null @@ -1,167 +0,0 @@ -package partner - -import ( - "context" - "enaklo-pos-be/internal/common/logger" - "enaklo-pos-be/internal/common/mycontext" - "enaklo-pos-be/internal/entity" - "enaklo-pos-be/internal/repository" - "enaklo-pos-be/internal/services/users" - "go.uber.org/zap" -) - -type PartnerService struct { - repo repository.PartnerRepository - trx repository.Trx - userSvc *users.UserService - walletRepo repository.WalletRepository - userRepo repository.User -} - -func NewPartnerService(repo repository.PartnerRepository, - userSvc *users.UserService, repoManager repository.Trx, - walletRepo repository.WalletRepository, - userRepo repository.User, -) *PartnerService { - return &PartnerService{ - repo: repo, - userSvc: userSvc, - trx: repoManager, - walletRepo: walletRepo, - userRepo: userRepo, - } -} - -func (s *PartnerService) Create(ctx mycontext.Context, partnerReq *entity.CreatePartnerRequest) (*entity.Partner, error) { - var err error - - tx, err := s.trx.Begin(ctx) - if err != nil { - logger.ContextLogger(ctx).Error("error when starting transaction", zap.Error(err)) - return nil, err - } - - defer func() { - if r := recover(); r != nil { - s.trx.Rollback(tx) - panic(r) - } else if err != nil { - s.trx.Rollback(tx) - } else { - err = s.trx.Commit(tx).Error - } - }() - - // Create Partner - partnerDB := partnerReq.ToPartnerDB(ctx.RequestedBy()) - if partnerDB, err = s.repo.CreateWithTx(ctx, tx, partnerDB); err != nil { - logger.ContextLogger(ctx).Error("error when creating partner", zap.Error(err)) - return nil, err - } - - adminUser := partnerReq.ToUserAdmin(partnerDB.ID) - if adminUser, err = s.userSvc.CreateWithTx(ctx, tx, adminUser); err != nil { - logger.ContextLogger(ctx).Error("error when creating admin user", zap.Error(err)) - return nil, err - } - - partnerDB.AdminUserID = adminUser.ID - if partnerDB, err = s.repo.UpdateWithTx(ctx, tx, partnerDB); err != nil { - logger.ContextLogger(ctx).Error("error when creating partner", zap.Error(err)) - return nil, err - } - - partnerWallet := partnerReq.ToWallet(partnerDB.ID) - if partnerWallet, err = s.walletRepo.Create(ctx, tx, partnerWallet); err != nil { - logger.ContextLogger(ctx).Error("error when creating wallet", zap.Error(err)) - return nil, err - } - - return partnerDB.ToPartner(), nil -} - -func (s *PartnerService) Update(ctx mycontext.Context, req *entity.PartnerUpdate) (*entity.Partner, error) { - existingPartner, err := s.repo.GetByID(ctx, req.ID) - if err != nil { - return nil, err - } - existingPartner.ToUpdatedPartnerData(ctx.RequestedBy(), *req) - - tx, err := s.trx.Begin(ctx) - if err != nil { - logger.ContextLogger(ctx).Error("error when starting transaction", zap.Error(err)) - return nil, err - } - - defer func() { - if r := recover(); r != nil { - s.trx.Rollback(tx) - panic(r) - } else if err != nil { - s.trx.Rollback(tx) - } else { - err = s.trx.Commit(tx).Error - } - }() - - updatedPartnerDB, err := s.repo.UpdateWithTx(ctx, tx, existingPartner.ToPartnerDB()) - if err != nil { - logger.ContextLogger(ctx).Error("error when update Partner", zap.Error(err)) - return nil, err - } - - partnerAdmin, err := s.userRepo.GetPartnerAdmin(ctx, req.ID) - if err != nil { - logger.ContextLogger(ctx).Error("error when get Partner admin", zap.Error(err)) - return nil, err - } - - if partnerAdmin != nil { - req.AdminUserID = partnerAdmin.ID - adminUser := req.ToUserAdmin(&req.ID) - if adminUser, err = s.userSvc.UpdateWithTx(ctx, tx, adminUser); err != nil { - logger.ContextLogger(ctx).Error("error when creating admin user", zap.Error(err)) - return nil, err - } - } - - return updatedPartnerDB.ToPartner(), nil -} - -func (s *PartnerService) GetByID(ctx context.Context, id int64) (*entity.Partner, error) { - PartnerDB, err := s.repo.GetByID(ctx, id) - if err != nil { - logger.ContextLogger(ctx).Error("error when get Partner by id", zap.Error(err)) - return nil, err - } - - return PartnerDB.ToPartner(), nil -} - -func (s *PartnerService) GetAll(ctx context.Context, search entity.PartnerSearch) ([]*entity.Partner, int, error) { - Partneres, total, err := s.repo.GetAll(ctx, search) - if err != nil { - logger.ContextLogger(ctx).Error("error when get all Partneres", zap.Error(err)) - return nil, 0, err - } - - return Partneres.ToPartnerList(), total, nil -} - -func (s *PartnerService) Delete(ctx mycontext.Context, id int64) error { - PartnerDB, err := s.repo.GetByID(ctx, id) - if err != nil { - logger.ContextLogger(ctx).Error("error when get Partner by id", zap.Error(err)) - return err - } - - PartnerDB.SetDeleted(ctx.RequestedBy()) - - _, err = s.repo.Update(ctx, PartnerDB) - if err != nil { - logger.ContextLogger(ctx).Error("error when update Partner", zap.Error(err)) - return err - } - - return nil -} diff --git a/internal/services/product/product.go b/internal/services/product/product.go deleted file mode 100644 index 0d5963f..0000000 --- a/internal/services/product/product.go +++ /dev/null @@ -1,99 +0,0 @@ -package product - -import ( - "context" - "enaklo-pos-be/internal/common/logger" - "enaklo-pos-be/internal/common/mycontext" - "enaklo-pos-be/internal/entity" - "enaklo-pos-be/internal/repository" - - "go.uber.org/zap" -) - -type ProductService struct { - repo repository.Product -} - -func NewProductService(repo repository.Product) *ProductService { - return &ProductService{ - repo: repo, - } -} - -func (s *ProductService) Create(ctx mycontext.Context, productReq *entity.Product) (*entity.Product, error) { - productDB := productReq.ToProductDB() - productDB.CreatedBy = ctx.RequestedBy() - - productDB, err := s.repo.CreateProduct(ctx, productDB) - if err != nil { - logger.ContextLogger(ctx).Error("error when create product", zap.Error(err)) - return nil, err - } - - return productDB.ToProduct(), nil -} - -func (s *ProductService) Update(ctx mycontext.Context, id int64, productReq *entity.Product) (*entity.Product, error) { - existingProduct, err := s.repo.GetProductByID(ctx, id) - if err != nil { - return nil, err - } - - existingProduct.ToUpdatedProduct(ctx.RequestedBy(), *productReq) - - updatedProductDB, err := s.repo.UpdateProduct(ctx, existingProduct.ToProductDB()) - if err != nil { - logger.ContextLogger(ctx).Error("error when update product", zap.Error(err)) - return nil, err - } - - return updatedProductDB.ToProduct(), nil -} - -func (s *ProductService) GetByID(ctx context.Context, id int64) (*entity.Product, error) { - productDB, err := s.repo.GetProductByID(ctx, id) - if err != nil { - logger.ContextLogger(ctx).Error("error when get product by id", zap.Error(err)) - return nil, err - } - - return productDB.ToProduct(), nil -} - -func (s *ProductService) GetAll(ctx context.Context, search entity.ProductSearch) ([]*entity.Product, int, error) { - products, total, err := s.repo.GetAllProducts(ctx, search) - if err != nil { - logger.ContextLogger(ctx).Error("error when get all products", zap.Error(err)) - return nil, 0, err - } - - return products.ToProductList(), total, nil -} - -func (s *ProductService) GetProductPOS(ctx context.Context, search entity.ProductPOS) ([]*entity.Product, error) { - products, err := s.repo.GetProductByPartnerIDAndSiteID(ctx, search.PartnerID, search.SiteID) - if err != nil { - logger.ContextLogger(ctx).Error("error when get all products", zap.Error(err)) - return nil, err - } - - return products.ToProductListPOS(), nil -} - -func (s *ProductService) Delete(ctx mycontext.Context, id int64) error { - productDB, err := s.repo.GetProductByID(ctx, id) - if err != nil { - logger.ContextLogger(ctx).Error("error when get product by id", zap.Error(err)) - return err - } - - productDB.SetDeleted(ctx.RequestedBy()) - - _, err = s.repo.UpdateProduct(ctx, productDB) - if err != nil { - logger.ContextLogger(ctx).Error("error when update product", zap.Error(err)) - return err - } - - return nil -} diff --git a/internal/services/service.go b/internal/services/service.go deleted file mode 100644 index bb8e0ac..0000000 --- a/internal/services/service.go +++ /dev/null @@ -1,154 +0,0 @@ -package services - -import ( - "context" - "enaklo-pos-be/internal/common/mycontext" - "enaklo-pos-be/internal/services/balance" - service "enaklo-pos-be/internal/services/license" - "enaklo-pos-be/internal/services/member" - "enaklo-pos-be/internal/services/oss" - "enaklo-pos-be/internal/services/partner" - "enaklo-pos-be/internal/services/product" - site "enaklo-pos-be/internal/services/sites" - "enaklo-pos-be/internal/services/transaction" - "enaklo-pos-be/internal/services/users" - authSvc "enaklo-pos-be/internal/services/v2/auth" - "enaklo-pos-be/internal/services/v2/cashier_session" - category "enaklo-pos-be/internal/services/v2/categories" - customerSvc "enaklo-pos-be/internal/services/v2/customer" - "enaklo-pos-be/internal/services/v2/inprogress_order" - orderSvc "enaklo-pos-be/internal/services/v2/order" - "enaklo-pos-be/internal/services/v2/undian" - - "enaklo-pos-be/internal/services/v2/partner_settings" - productSvc "enaklo-pos-be/internal/services/v2/product" - - "gorm.io/gorm" - - "enaklo-pos-be/config" - "enaklo-pos-be/internal/entity" - "enaklo-pos-be/internal/repository" - "enaklo-pos-be/internal/services/auth" -) - -type ServiceManagerImpl struct { - AuthSvc Auth - UserSvc User - ProductSvc Product - OSSSvc OSSService - PartnerSvc Partner - SiteSvc Site - LicenseSvc License - Transaction Transaction - Balance Balance - - OrderV2Svc orderSvc.Service - CustomerV2Svc customerSvc.Service - ProductV2Svc productSvc.Service - MemberRegistrationSvc member.RegistrationService - InProgressSvc inprogress_order.InProgressOrderService - AuthV2Svc authSvc.Service - UndianSvc undian.Service - CashierSvc cashier_session.Service - CategorySvc category.Service -} - -func NewServiceManagerImpl(cfg *config.Config, repo *repository.RepoManagerImpl) *ServiceManagerImpl { - - custSvcV2 := customerSvc.New(repo.CustomerRepo, repo.EmailService) - productSvcV2 := productSvc.New(repo.ProductRepo) - partnerSettings := partner_settings.NewPartnerSettingsService(repo.PartnerSetting) - cashierSvc := cashier_session.New(repo.CashierSeasionRepo) - orderService := orderSvc.New(repo.OrderRepo, - productSvcV2, custSvcV2, repo.TransactionRepo, - repo.Crypto, &cfg.Order, repo.EmailService, partnerSettings, - repo.UndianRepository, cashierSvc) - inprogressOrder := inprogress_order.NewInProgressOrderService(repo.OrderRepo, orderService, productSvcV2, repo.Trx) - categorySvc := category.New(repo.CategoryRepository) - return &ServiceManagerImpl{ - AuthSvc: auth.New(repo.Auth, repo.Crypto, repo.User, repo.EmailService, cfg.Email, repo.Trx, repo.License), - UserSvc: users.NewUserService(repo.User), - ProductSvc: product.NewProductService(repo.Product), - OSSSvc: oss.NewOSSService(repo.OSS), - PartnerSvc: partner.NewPartnerService( - repo.Partner, users.NewUserService(repo.User), repo.Trx, repo.Wallet, repo.User), - SiteSvc: site.NewSiteService(repo.Site, repo.User), - LicenseSvc: service.NewLicenseService(repo.License), - Transaction: transaction.New(repo.Transaction, repo.Wallet, repo.Trx), - Balance: balance.NewBalanceService(repo.Wallet, repo.Trx, repo.Crypto, &cfg.Withdraw, repo.Transaction), - OrderV2Svc: orderSvc.New(repo.OrderRepo, productSvcV2, custSvcV2, repo.TransactionRepo, repo.Crypto, &cfg.Order, repo.EmailService, partnerSettings, repo.UndianRepository, cashierSvc), - MemberRegistrationSvc: member.NewMemberRegistrationService(repo.MemberRepository, repo.EmailService, custSvcV2, repo.Crypto), - CustomerV2Svc: custSvcV2, - InProgressSvc: inprogressOrder, - ProductV2Svc: productSvcV2, - AuthV2Svc: authSvc.New(repo.CustomerRepo, repo.Crypto), - UndianSvc: undian.New(repo.UndianRepository), - CashierSvc: cashierSvc, - CategorySvc: categorySvc, - } -} - -type Auth interface { - AuthenticateUser(ctx context.Context, email, password string) (*entity.AuthenticateUser, error) - SendPasswordResetLink(ctx context.Context, email string) error - ResetPassword(ctx mycontext.Context, oldPassword, newPassword string) error -} - -type User interface { - Create(ctx mycontext.Context, userReq *entity.User) (*entity.User, error) - CreateWithTx(ctx mycontext.Context, tx *gorm.DB, userReq *entity.User) (*entity.User, error) - GetAll(ctx mycontext.Context, search entity.UserSearch) ([]*entity.User, int, error) - GetAllCustomer(ctx mycontext.Context, search entity.CustomerSearch) ([]*entity.Customer, int, error) - Update(ctx mycontext.Context, id int64, userReq *entity.User) (*entity.User, error) - UpdateWithTx(ctx mycontext.Context, tx *gorm.DB, req *entity.User) (*entity.User, error) - GetByID(ctx mycontext.Context, id int64) (*entity.User, error) - Delete(ctx mycontext.Context, id int64) error -} - -type Product interface { - Create(ctx mycontext.Context, productReq *entity.Product) (*entity.Product, error) - Update(ctx mycontext.Context, id int64, productReq *entity.Product) (*entity.Product, error) - GetByID(ctx context.Context, id int64) (*entity.Product, error) - GetAll(ctx context.Context, search entity.ProductSearch) ([]*entity.Product, int, error) - GetProductPOS(ctx context.Context, search entity.ProductPOS) ([]*entity.Product, error) - Delete(ctx mycontext.Context, id int64) error -} - -type OSSService interface { - UploadFile(ctx context.Context, req *entity.UploadFileRequest) (*entity.UploadFileResponse, error) -} - -type Partner interface { - Create(ctx mycontext.Context, branchReq *entity.CreatePartnerRequest) (*entity.Partner, error) - Update(ctx mycontext.Context, branchReq *entity.PartnerUpdate) (*entity.Partner, error) - GetByID(ctx context.Context, id int64) (*entity.Partner, error) - GetAll(ctx context.Context, search entity.PartnerSearch) ([]*entity.Partner, int, error) - Delete(ctx mycontext.Context, id int64) error -} - -type Site interface { - Create(ctx mycontext.Context, branchReq *entity.Site) (*entity.Site, error) - Update(ctx mycontext.Context, id int64, branchReq *entity.Site) (*entity.Site, error) - GetByID(ctx context.Context, id int64) (*entity.Site, error) - GetAll(ctx context.Context, search entity.SiteSearch) ([]*entity.Site, int, error) - Delete(ctx mycontext.Context, id int64) error - Count(ctx mycontext.Context, req entity.SiteSearch) (*entity.SiteCount, error) -} - -type License interface { - Create(ctx mycontext.Context, licenseReq *entity.License) (*entity.License, error) - Update(ctx mycontext.Context, id string, licenseReq *entity.License) (*entity.License, error) - GetByID(ctx context.Context, id string) (*entity.License, error) - GetAll(ctx context.Context, limit, offset int, status string) ([]*entity.LicenseGetAll, int64, error) -} - -type Transaction interface { - GetTransactionList(ctx mycontext.Context, req entity.TransactionSearch) ([]*entity.TransactionList, int, error) - Approval(ctx mycontext.Context, req *entity.TransactionApproval) error -} - -type Balance interface { - GetByID(ctx context.Context, id int64) (*entity.Balance, error) - WithdrawInquiry(ctx context.Context, req *entity.BalanceWithdrawInquiry) (*entity.BalanceWithdrawInquiryResponse, error) - WithdrawExecute(ctx mycontext.Context, req *entity.WalletWithdrawRequest) (*entity.WalletWithdrawResponse, error) -} diff --git a/internal/services/sites/sites.go b/internal/services/sites/sites.go deleted file mode 100644 index 13a42d6..0000000 --- a/internal/services/sites/sites.go +++ /dev/null @@ -1,112 +0,0 @@ -package site - -import ( - "context" - "enaklo-pos-be/internal/common/logger" - "enaklo-pos-be/internal/common/mycontext" - "enaklo-pos-be/internal/constants/role" - "enaklo-pos-be/internal/entity" - "enaklo-pos-be/internal/repository" - "go.uber.org/zap" -) - -type SiteService struct { - repo repository.SiteRepository - user repository.User -} - -func NewSiteService(repo repository.SiteRepository, user repository.User) *SiteService { - return &SiteService{ - repo: repo, - user: user, - } -} - -func (s *SiteService) Create(ctx mycontext.Context, siteReq *entity.Site) (*entity.Site, error) { - siteDB := siteReq - siteDB.CreatedBy = ctx.RequestedBy() - - siteDB, err := s.repo.Upsert(ctx, siteDB) - if err != nil { - logger.ContextLogger(ctx).Error("error when creating site", zap.Error(err)) - return nil, err - } - - return siteDB, nil -} - -func (s *SiteService) Update(ctx mycontext.Context, id int64, siteReq *entity.Site) (*entity.Site, error) { - existingSite, err := s.repo.GetByID(ctx, id) - if err != nil { - return nil, err - } - - existingSite.ToUpdatedSite(ctx.RequestedBy(), *siteReq) - - updatedSiteDB, err := s.repo.Update(ctx, existingSite.ToSiteDB()) - if err != nil { - logger.ContextLogger(ctx).Error("error when updating site", zap.Error(err)) - return nil, err - } - - return updatedSiteDB.ToSite(), nil -} - -func (s *SiteService) GetByID(ctx context.Context, id int64) (*entity.Site, error) { - siteDB, err := s.repo.GetByID(ctx, id) - if err != nil { - logger.ContextLogger(ctx).Error("error when getting site by ID", zap.Error(err)) - return nil, err - } - - return siteDB.ToSite(), nil -} - -func (s *SiteService) GetAll(ctx context.Context, search entity.SiteSearch) ([]*entity.Site, int, error) { - sites, total, err := s.repo.GetAll(ctx, search) - if err != nil { - logger.ContextLogger(ctx).Error("error when getting all sites", zap.Error(err)) - return nil, 0, err - } - - return sites.ToSiteList(), total, nil -} - -func (s *SiteService) Delete(ctx mycontext.Context, id int64) error { - siteDB, err := s.repo.GetByID(ctx, id) - if err != nil { - logger.ContextLogger(ctx).Error("error when getting site by ID", zap.Error(err)) - return err - } - - siteDB.SetDeleted(ctx.RequestedBy()) - - _, err = s.repo.Update(ctx, siteDB) - if err != nil { - logger.ContextLogger(ctx).Error("error when updating site", zap.Error(err)) - return err - } - - return nil -} - -func (s *SiteService) Count(ctx mycontext.Context, req entity.SiteSearch) (*entity.SiteCount, error) { - if req.SiteID != nil { - total, err := s.user.CountUsersByRoleAndSiteOrPartner(ctx, int(role.Casheer), req.SiteID) - if err != nil { - logger.ContextLogger(ctx).Error("error when getting count user", zap.Error(err)) - return nil, err - } - return &entity.SiteCount{ - Count: total, - }, nil - } - - count, err := s.repo.Count(ctx, req) - if err != nil { - logger.ContextLogger(ctx).Error("error when getting all sites", zap.Error(err)) - return nil, err - } - - return count.ToSiteCount(), nil -} diff --git a/internal/services/transaction/transaction.go b/internal/services/transaction/transaction.go deleted file mode 100644 index 9649f61..0000000 --- a/internal/services/transaction/transaction.go +++ /dev/null @@ -1,111 +0,0 @@ -package transaction - -import ( - errors2 "enaklo-pos-be/internal/common/errors" - "enaklo-pos-be/internal/common/logger" - "enaklo-pos-be/internal/common/mycontext" - "enaklo-pos-be/internal/entity" - "enaklo-pos-be/internal/repository" - - "go.uber.org/zap" -) - -type TransactionService struct { - repo repository.TransactionRepository - wallet repository.WalletRepository - trx repository.Trx -} - -func New(repo repository.TransactionRepository, - wallet repository.WalletRepository, - trx repository.Trx, -) *TransactionService { - return &TransactionService{ - repo: repo, - wallet: wallet, - trx: trx, - } -} - -func (s *TransactionService) GetTransactionList(ctx mycontext.Context, - req entity.TransactionSearch) ([]*entity.TransactionList, int, error) { - transactions, total, err := s.repo.GetTransactionList(ctx, req) - if err != nil { - logger.ContextLogger(ctx).Error("error when get all products", zap.Error(err)) - return nil, 0, err - } - - return transactions, total, nil -} - -func (s *TransactionService) Approval(ctx mycontext.Context, req *entity.TransactionApproval) error { - // Start a transaction - trx, _ := s.trx.Begin(ctx) - - // Retrieve the transaction by ID - transaction, err := s.repo.FindByID(ctx, req.TransactionID) - if err != nil { - logger.ContextLogger(ctx).Error("error when retrieving transaction by ID", zap.Error(err)) - trx.Rollback() - return errors2.ErrorInternalServer - } - - if transaction.Status != "WAITING_APPROVAL" { - return errors2.NewError(errors2.ErrorBadRequest.ErrorType(), - "invalid state, transaction already approved or rejected") - } - - // Retrieve the wallet associated with the transaction's partner ID - wallet, err := s.wallet.GetForUpdate(ctx, trx, transaction.PartnerID) - if err != nil { - logger.ContextLogger(ctx).Error("error retrieving wallet by partner ID", zap.Error(err)) - trx.Rollback() - return errors2.ErrorInternalServer - } - - // Approve or Reject the transaction - switch req.Status { - case "APPROVE": - if wallet.AuthBalance < transaction.Amount { - trx.Rollback() - return errors2.ErrorInsufficientBalance - } - wallet.AuthBalance -= transaction.Amount - transaction.Status = "APPROVED" - case "REJECT": - if wallet.AuthBalance < transaction.Amount { - trx.Rollback() - return errors2.ErrorInsufficientBalance - } - transaction.Status = "REJECTED" - wallet.AuthBalance -= transaction.Amount - wallet.Balance += transaction.Amount - - default: - trx.Rollback() - return errors2.ErrorBadRequest - } - - // Update the wallet with the new balances - if _, err := s.wallet.Update(ctx, trx, wallet); err != nil { - logger.ContextLogger(ctx).Error("error updating wallet balance", zap.Error(err)) - trx.Rollback() - return errors2.ErrorInternalServer - } - - // Update the transaction status and persist changes - transaction.UpdatedBy = ctx.RequestedBy() - - if _, err := s.repo.Update(ctx, trx, transaction); err != nil { - logger.ContextLogger(ctx).Error("error updating transaction status", zap.Error(err)) - trx.Rollback() - return errors2.ErrorInternalServer - } - - if err := trx.Commit().Error; err != nil { - logger.ContextLogger(ctx).Error("error committing transaction", zap.Error(err)) - return errors2.ErrorInternalServer - } - - return nil -} diff --git a/internal/services/users/users.go b/internal/services/users/users.go deleted file mode 100644 index 20ea28a..0000000 --- a/internal/services/users/users.go +++ /dev/null @@ -1,183 +0,0 @@ -package users - -import ( - "enaklo-pos-be/internal/common/mycontext" - "errors" - "fmt" - - "gorm.io/gorm" - - "go.uber.org/zap" - - "enaklo-pos-be/internal/common/logger" - "enaklo-pos-be/internal/entity" - "enaklo-pos-be/internal/repository" -) - -type UserService struct { - repo repository.User -} - -func NewUserService(repo repository.User) *UserService { - return &UserService{ - repo: repo, - } -} - -func (s *UserService) Create(ctx mycontext.Context, userReq *entity.User) (*entity.User, error) { - //check - userExist, err := s.repo.GetUserByEmail(ctx, userReq.Email) - if err != nil { - return nil, err - } - - if userExist != nil { - return nil, fmt.Errorf("Email already exist") - } - - userDB, err := userReq.ToUserDB(ctx.RequestedBy()) - if err != nil { - return nil, err - } - - userDB, err = s.repo.Create(ctx, userDB) - if err != nil { - logger.ContextLogger(ctx).Error("error when create user", zap.Error(err)) - return nil, err - } - - return userDB.ToUser(), nil -} - -func (s *UserService) CreateWithTx(ctx mycontext.Context, tx *gorm.DB, userReq *entity.User) (*entity.User, error) { - //check - userExist, err := s.repo.GetUserByEmail(ctx, userReq.Email) - if err != nil { - return nil, err - } - - if userExist != nil { - return nil, fmt.Errorf("Email already exist") - } - - userDB, err := userReq.ToUserDB(ctx.RequestedBy()) - if err != nil { - return nil, err - } - - userDB, err = s.repo.CreateWithTx(ctx, tx, userDB) - if err != nil { - logger.ContextLogger(ctx).Error("error when create user", zap.Error(err)) - return nil, err - } - - return userDB.ToUser(), nil -} - -func (s *UserService) GetAll(ctx mycontext.Context, search entity.UserSearch) ([]*entity.User, int, error) { - - users, total, err := s.repo.GetAllUsers(ctx, search) - if err != nil { - logger.ContextLogger(ctx).Error("error when get all users", zap.Error(err)) - return nil, 0, err - } - - return users.ToUserList(), total, nil -} - -func (s *UserService) GetAllCustomer(ctx mycontext.Context, search entity.CustomerSearch) ([]*entity.Customer, int, error) { - - users, total, err := s.repo.GetAllCustomer(ctx, search) - if err != nil { - logger.ContextLogger(ctx).Error("error when get all users", zap.Error(err)) - return nil, 0, err - } - - return users.ToCustomerList(), total, nil -} - -func (s *UserService) GetByID(ctx mycontext.Context, id int64) (*entity.User, error) { - userDB, err := s.repo.GetUserByID(ctx, id) - if err != nil { - logger.ContextLogger(ctx).Error("error when get user by id", zap.Error(err)) - return nil, err - } - - return userDB.ToUser(), nil -} - -func (s *UserService) Update(ctx mycontext.Context, id int64, userReq *entity.User) (*entity.User, error) { - existingUser, err := s.repo.GetUserByID(ctx, id) - if err != nil { - return nil, err - } - - if existingUser == nil { - return nil, errors.New("user not found") - } - - if !ctx.IsAdmin() && userReq.UserType != "CUSTOMER" { - if *existingUser.PartnerID != *userReq.PartnerID { - return nil, errors.New("user partner cant be changed") - } - } - - err = existingUser.ToUpdatedUser(*userReq) - if err != nil { - return nil, err - } - - updatedUserDB, err := s.repo.UpdateUser(ctx, existingUser) - if err != nil { - logger.ContextLogger(ctx).Error("error when update user", zap.Error(err)) - return nil, err - } - - return updatedUserDB.ToUser(), nil -} - -func (s *UserService) UpdateWithTx(ctx mycontext.Context, tx *gorm.DB, req *entity.User) (*entity.User, error) { - existingUser, err := s.repo.GetUserByID(ctx, req.ID) - if err != nil { - return nil, err - } - - if existingUser == nil { - return nil, errors.New("user not found") - } - - if *existingUser.PartnerID != *req.PartnerID { - return nil, errors.New("invalid request") - } - - err = existingUser.ToUpdatedUser(*req) - if err != nil { - return nil, err - } - - updatedUserDB, err := s.repo.UpdateUserWithTx(ctx, tx, existingUser) - if err != nil { - logger.ContextLogger(ctx).Error("error when update user", zap.Error(err)) - return nil, err - } - - return updatedUserDB.ToUser(), nil -} - -func (s *UserService) Delete(ctx mycontext.Context, id int64) error { - userDB, err := s.repo.GetUserByID(ctx, id) - if err != nil { - logger.ContextLogger(ctx).Error("error when get user by id", zap.Error(err)) - return err - } - - userDB.SetDeleted(ctx.RequestedBy()) - - _, err = s.repo.UpdateUser(ctx, userDB) - if err != nil { - logger.ContextLogger(ctx).Error("error when update user", zap.Error(err)) - return err - } - - return nil -} diff --git a/internal/services/v2/auth/auth.go b/internal/services/v2/auth/auth.go deleted file mode 100644 index 6d8e271..0000000 --- a/internal/services/v2/auth/auth.go +++ /dev/null @@ -1,58 +0,0 @@ -package auth - -import ( - "enaklo-pos-be/internal/common/errors" - "enaklo-pos-be/internal/common/logger" - "enaklo-pos-be/internal/common/mycontext" - "enaklo-pos-be/internal/entity" - "go.uber.org/zap" -) - -type Repository interface { - FindByEmail(ctx mycontext.Context, email string) (*entity.Customer, error) -} - -type CryptoSvc interface { - GenerateJWTCustomer(user *entity.Customer) (string, error) - CompareHashAndPassword(hash string, password string) bool -} - -type Service interface { - AuthCustomer(ctx mycontext.Context, email, password string) (*entity.AuthenticateUser, error) -} - -type authSvc struct { - repo Repository - crypt CryptoSvc -} - -func New(repo Repository, cryptSvc CryptoSvc) Service { - return &authSvc{ - repo: repo, - crypt: cryptSvc, - } -} - -func (a authSvc) AuthCustomer(ctx mycontext.Context, email, password string) (*entity.AuthenticateUser, error) { - user, err := a.repo.FindByEmail(ctx, email) - if err != nil { - logger.ContextLogger(ctx).Error("error when get user", zap.Error(err)) - return nil, errors.ErrorInternalServer - } - - if user == nil { - return nil, errors.ErrorUserIsNotFound - } - - if ok := a.crypt.CompareHashAndPassword(user.Password, password); !ok { - return nil, errors.ErrorUserInvalidLogin - } - - signedToken, err := a.crypt.GenerateJWTCustomer(user) - - if err != nil { - return nil, err - } - - return user.ToUserAuthenticate(signedToken), nil -} diff --git a/internal/services/v2/cashier_session/casheer_session.go b/internal/services/v2/cashier_session/casheer_session.go deleted file mode 100644 index 07d8767..0000000 --- a/internal/services/v2/cashier_session/casheer_session.go +++ /dev/null @@ -1,110 +0,0 @@ -package cashier_session - -import ( - "enaklo-pos-be/internal/common/logger" - "enaklo-pos-be/internal/common/mycontext" - "enaklo-pos-be/internal/entity" - - "github.com/pkg/errors" - "go.uber.org/zap" -) - -type Service interface { - OpenSession(ctx mycontext.Context, session *entity.CashierSession) (*entity.CashierSession, error) - CloseSession(ctx mycontext.Context, sessionID int64, closingAmount float64) (*entity.CashierSessionReport, error) - GetOpenSession(ctx mycontext.Context, cashierID int64) (*entity.CashierSession, error) - GetSessionReport(ctx mycontext.Context, sessionID int64) (*entity.CashierSessionReport, error) - GetSessionHistory(ctx mycontext.Context, partnerID int64, limit, offset int) ([]*entity.CashierSession, int64, error) -} - -type Repository interface { - CreateSession(ctx mycontext.Context, session *entity.CashierSession) (*entity.CashierSession, error) - CloseSession(ctx mycontext.Context, sessionID int64, closingAmount, expectedAmount float64) error - GetOpenSessionByCashierID(ctx mycontext.Context, cashierID int64) (*entity.CashierSession, error) - GetSessionByID(ctx mycontext.Context, sessionID int64) (*entity.CashierSession, error) - GetPaymentSummaryBySessionID(ctx mycontext.Context, sessionID int64) ([]entity.PaymentSummary, error) - GetSessionHistoryByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int) ([]*entity.CashierSession, int64, error) -} - -type cashierSessionSvc struct { - repo Repository -} - -func New(repo Repository) Service { - return &cashierSessionSvc{repo: repo} -} - -func (s *cashierSessionSvc) OpenSession(ctx mycontext.Context, session *entity.CashierSession) (*entity.CashierSession, error) { - openSession, err := s.repo.GetOpenSessionByCashierID(ctx, session.CashierID) - if err != nil { - return nil, errors.Wrap(err, "failed to check existing open session") - } - if openSession != nil { - return nil, errors.New("cashier already has an open session") - } - - newSession, err := s.repo.CreateSession(ctx, session) - if err != nil { - logger.ContextLogger(ctx).Error("failed to create cashier session", zap.Error(err)) - return nil, errors.Wrap(err, "failed to create cashier session") - } - - return newSession, nil -} - -func (s *cashierSessionSvc) CloseSession(ctx mycontext.Context, sessionID int64, closingAmount float64) (*entity.CashierSessionReport, error) { - report, err := s.repo.GetPaymentSummaryBySessionID(ctx, sessionID) - if err != nil { - return nil, errors.Wrap(err, "failed to get payment summary") - } - - var expectedAmount float64 - for _, r := range report { - expectedAmount += r.TotalAmount - } - - if err := s.repo.CloseSession(ctx, sessionID, closingAmount, expectedAmount); err != nil { - return nil, errors.Wrap(err, "failed to close session") - } - - return &entity.CashierSessionReport{ - SessionID: sessionID, - ClosingAmount: closingAmount, - ExpectedAmount: expectedAmount, - Payments: report, - }, nil -} - -func (s *cashierSessionSvc) GetOpenSession(ctx mycontext.Context, cashierID int64) (*entity.CashierSession, error) { - session, err := s.repo.GetOpenSessionByCashierID(ctx, cashierID) - if err != nil { - return nil, errors.Wrap(err, "failed to get open session") - } - return session, nil -} - -func (s *cashierSessionSvc) GetSessionReport(ctx mycontext.Context, sessionID int64) (*entity.CashierSessionReport, error) { - report, err := s.repo.GetPaymentSummaryBySessionID(ctx, sessionID) - if err != nil { - return nil, errors.Wrap(err, "failed to get payment summary") - } - - var expectedAmount float64 - for _, r := range report { - expectedAmount += r.TotalAmount - } - - return &entity.CashierSessionReport{ - SessionID: sessionID, - ExpectedAmount: expectedAmount, - Payments: report, - }, nil -} - -func (s *cashierSessionSvc) GetSessionHistory(ctx mycontext.Context, partnerID int64, limit, offset int) ([]*entity.CashierSession, int64, error) { - sessions, total, err := s.repo.GetSessionHistoryByPartnerID(ctx, partnerID, limit, offset) - if err != nil { - return nil, 0, errors.Wrap(err, "failed to get session history") - } - return sessions, total, nil -} diff --git a/internal/services/v2/categories/categories.go b/internal/services/v2/categories/categories.go deleted file mode 100644 index f526870..0000000 --- a/internal/services/v2/categories/categories.go +++ /dev/null @@ -1,88 +0,0 @@ -package category - -import ( - "enaklo-pos-be/internal/common/logger" - "enaklo-pos-be/internal/common/mycontext" - "enaklo-pos-be/internal/entity" - "github.com/pkg/errors" - "go.uber.org/zap" -) - -type Service interface { - Create(ctx mycontext.Context, category *entity.Category) (*entity.Category, error) - GetByPartnerID(ctx mycontext.Context, partnerID int64) ([]*entity.Category, error) - Update(ctx mycontext.Context, category *entity.Category) error - Delete(ctx mycontext.Context, id int64) error - GetByID(ctx mycontext.Context, id int64) (*entity.Category, error) -} - -type Repository interface { - Create(ctx mycontext.Context, category *entity.Category) (*entity.Category, error) - GetByPartnerID(ctx mycontext.Context, partnerID int64) ([]*entity.Category, error) - Update(ctx mycontext.Context, category *entity.Category) error - Delete(ctx mycontext.Context, id int64) error - GetByID(ctx mycontext.Context, id int64) (*entity.Category, error) -} - -type categorySvc struct { - repo Repository -} - -func New(repo Repository) Service { - return &categorySvc{repo: repo} -} - -func (s *categorySvc) Create(ctx mycontext.Context, category *entity.Category) (*entity.Category, error) { - existing, err := s.repo.GetByPartnerID(ctx, category.PartnerID) - if err != nil { - return nil, errors.Wrap(err, "failed to fetch categories") - } - - for _, cat := range existing { - if cat.Name == category.Name { - return nil, errors.New("category name already exists for this partner") - } - } - - newCategory, err := s.repo.Create(ctx, category) - if err != nil { - logger.ContextLogger(ctx).Error("failed to create category", zap.Error(err)) - return nil, errors.Wrap(err, "failed to create category") - } - - return newCategory, nil -} - -func (s *categorySvc) GetByPartnerID(ctx mycontext.Context, partnerID int64) ([]*entity.Category, error) { - categories, err := s.repo.GetByPartnerID(ctx, partnerID) - if err != nil { - return nil, errors.Wrap(err, "failed to get categories by partner") - } - return categories, nil -} - -func (s *categorySvc) Update(ctx mycontext.Context, category *entity.Category) error { - err := s.repo.Update(ctx, category) - if err != nil { - logger.ContextLogger(ctx).Error("failed to update category", zap.Error(err)) - return errors.Wrap(err, "failed to update category") - } - return nil -} - -func (s *categorySvc) Delete(ctx mycontext.Context, id int64) error { - err := s.repo.Delete(ctx, id) - if err != nil { - logger.ContextLogger(ctx).Error("failed to delete category", zap.Error(err)) - return errors.Wrap(err, "failed to delete category") - } - return nil -} - -func (s *categorySvc) GetByID(ctx mycontext.Context, id int64) (*entity.Category, error) { - category, err := s.repo.GetByID(ctx, id) - if err != nil { - return nil, errors.Wrap(err, "failed to get category by ID") - } - return category, nil -} diff --git a/internal/services/v2/customer/customer.go b/internal/services/v2/customer/customer.go deleted file mode 100644 index b187874..0000000 --- a/internal/services/v2/customer/customer.go +++ /dev/null @@ -1,360 +0,0 @@ -package customer - -import ( - "context" - errors2 "enaklo-pos-be/internal/common/errors" - "enaklo-pos-be/internal/common/logger" - "enaklo-pos-be/internal/common/mycontext" - "enaklo-pos-be/internal/constants" - "enaklo-pos-be/internal/entity" - "enaklo-pos-be/internal/utils" - "github.com/pkg/errors" - "go.uber.org/zap" - "log" - "strings" - "time" -) - -type Repository interface { - Create(ctx mycontext.Context, customer *entity.Customer) (*entity.Customer, error) - FindByID(ctx mycontext.Context, id int64) (*entity.Customer, error) - FindByPhone(ctx mycontext.Context, phone string) (*entity.Customer, error) - FindByEmail(ctx mycontext.Context, email string) (*entity.Customer, error) - AddPoints(ctx mycontext.Context, id int64, points int, reference string) error - GetPointsByCustomerID( - ctx mycontext.Context, - customerID int64, - ) (*entity.CustomerPoints, error) - FindSequence(ctx mycontext.Context, partnerID int64) (int64, error) - GetAllCustomers(ctx mycontext.Context, req entity.MemberSearch) (entity.MemberList, int, error) - VerifyOTP(ctx mycontext.Context, verificationHash string, otpCode string) (int64, error) -} - -type Service interface { - ResolveCustomer(ctx mycontext.Context, req *entity.CustomerResolutionRequest) (int64, error) - AddPoints(ctx mycontext.Context, customerID int64, points int, reference string) error - GetCustomerPoints(ctx mycontext.Context, customerID int64) (*entity.CustomerPoints, error) - GetCustomer(ctx mycontext.Context, id int64) (*entity.Customer, error) - CustomerCheck(ctx mycontext.Context, req *entity.CustomerResolutionRequest) (*entity.CustomerCheckResponse, error) - GetAllCustomers(ctx mycontext.Context, req *entity.MemberSearch) (*entity.MemberList, int, error) - RegistrationMember(ctx mycontext.Context, req *entity.Customer) (*entity.Customer, error) - VerifyOTP(ctx mycontext.Context, verificationID, otpCode string) error -} - -type EmailService interface { - SendEmailTransactional(ctx context.Context, param entity.SendEmailNotificationParam) error -} - -type customerSvc struct { - repo Repository - notification EmailService -} - -func New(repo Repository, notification EmailService) Service { - return &customerSvc{ - repo: repo, - notification: notification, - } -} - -func (s *customerSvc) ResolveCustomer(ctx mycontext.Context, req *entity.CustomerResolutionRequest) (int64, error) { - if req.Email == "" && req.PhoneNumber == "" { - return 0, nil - } - - if req.ID != nil && *req.ID > 0 { - customer, err := s.repo.FindByID(ctx, *req.ID) - if err != nil { - if !strings.Contains(err.Error(), "not found") { - return 0, errors.Wrap(err, "failed to find customer by ID") - } - } else { - return customer.ID, nil - } - } - - if req.PhoneNumber != "" { - customer, err := s.repo.FindByPhone(ctx, req.PhoneNumber) - if err != nil { - if !strings.Contains(err.Error(), "not found") { - return 0, errors.Wrap(err, "failed to find customer by phone") - } - } else { - return customer.ID, nil - } - } - - if req.Email != "" { - customer, err := s.repo.FindByEmail(ctx, req.Email) - if err != nil { - if !strings.Contains(err.Error(), "not found") { - return 0, errors.Wrap(err, "failed to find customer by email") - } - } else { - return customer.ID, nil - } - } - - if req.Name == "" { - return 0, errors.New("customer name is required to create a new customer") - } - - lastSeq, err := s.repo.FindSequence(ctx, 1) - if err != nil { - return 0, errors.New("failed to resolve customer sequence") - } - - newCustomer := &entity.Customer{ - Name: req.Name, - Email: req.Email, - Phone: req.PhoneNumber, - Points: 0, - CreatedAt: constants.TimeNow(), - UpdatedAt: constants.TimeNow(), - CustomerID: utils.GenerateMemberID(ctx, 1, lastSeq), - BirthDate: req.BirthDate, - Password: req.Password, - } - - customer, err := s.repo.Create(ctx, newCustomer) - if err != nil { - logger.ContextLogger(ctx).Error("failed to create customer", zap.Error(err)) - return 0, errors.Wrap(err, "failed to create customer") - } - - return customer.ID, nil -} - -func (s *customerSvc) RegistrationMember(ctx mycontext.Context, req *entity.Customer) (*entity.Customer, error) { - if req.Email == "" && req.PhoneNumber == "" { - return nil, errors2.ErrorPhoneNumberEmailIsRequired - } - - if req.PhoneNumber != "" { - customer, err := s.repo.FindByPhone(ctx, req.PhoneNumber) - if err != nil && !strings.Contains(err.Error(), "not found") { - return nil, errors2.ErrorInternalServer - } - - if customer != nil { - return nil, errors2.ErrorPhoneNumberIsAlreadyRegistered - } - } - - if req.Email != "" { - customer, err := s.repo.FindByEmail(ctx, req.Email) - if err != nil && !strings.Contains(err.Error(), "not found") { - return nil, errors2.ErrorInternalServer - } - - if customer != nil { - return nil, errors2.ErrorEmailIsAlreadyRegistered - } - } - - newCustomer := &entity.Customer{ - Name: req.Name, - Email: req.Email, - Phone: req.PhoneNumber, - CreatedAt: constants.TimeNow(), - UpdatedAt: constants.TimeNow(), - BirthDate: req.BirthDate, - Password: req.HashedPassword(), - } - - customer, err := s.repo.Create(ctx, newCustomer) - if err != nil { - logger.ContextLogger(ctx).Error("failed to create customer", zap.Error(err)) - return nil, errors2.ErrorInternalServer - } - - errs := s.sendRegistrationOTP(ctx, &entity.MemberRegistration{ - Name: customer.Name, - Email: customer.Email, - OTP: customer.OTP, - }) - - if err != nil { - logger.ContextLogger(ctx).Error("failed to send OTP", zap.Error(errs)) - } - - return customer, nil -} - -func (s *customerSvc) AddPoints(ctx mycontext.Context, customerID int64, points int, reference string) error { - if points <= 0 { - return nil - } - - err := s.repo.AddPoints(ctx, customerID, points, reference) - if err != nil { - return errors.Wrap(err, "failed to add points to customer") - } - - return nil -} - -func (s *customerSvc) GetCustomerPoints(ctx mycontext.Context, customerID int64) (*entity.CustomerPoints, error) { - cp, err := s.repo.GetPointsByCustomerID(ctx, customerID) - if err != nil { - return nil, errors.Wrap(err, "failed to add points to customer") - } - - return cp, nil -} - -func (s *customerSvc) GetCustomer(ctx mycontext.Context, id int64) (*entity.Customer, error) { - customer, err := s.repo.FindByID(ctx, id) - if err != nil { - return nil, errors.Wrap(err, "failed to get customer") - } - - return customer, nil -} - -func (s *customerSvc) CustomerCheck(ctx mycontext.Context, req *entity.CustomerResolutionRequest) (*entity.CustomerCheckResponse, error) { - logger.ContextLogger(ctx).Info("checking customer existence before registration", - zap.String("email", req.Email), - zap.String("phone", req.PhoneNumber)) - - if req.Email == "" && req.PhoneNumber == "" { - return nil, errors.New("email dan phone number is mandatory") - } - - response := &entity.CustomerCheckResponse{ - Exists: false, - Customer: nil, - } - - if req.PhoneNumber != "" { - customer, err := s.repo.FindByPhone(ctx, req.PhoneNumber) - if err != nil { - if !strings.Contains(err.Error(), "not found") { - logger.ContextLogger(ctx).Error("error checking customer by phone", zap.Error(err)) - return nil, errors.Wrap(err, "failed to find customer by phone") - } - } else { - logger.ContextLogger(ctx).Info("found existing customer by phone", - zap.Int64("customerId", customer.ID)) - - return &entity.CustomerCheckResponse{ - Exists: true, - Customer: customer, - Message: "Nomor telepon sudah terdaftar. Silakan gunakan nomor lain atau login.", - }, nil - } - } - - if req.Email != "" { - customer, err := s.repo.FindByEmail(ctx, req.Email) - if err != nil { - if !strings.Contains(err.Error(), "not found") { - logger.ContextLogger(ctx).Error("error checking customer by email", zap.Error(err)) - return nil, errors.Wrap(err, "failed to find customer by email") - } - } else { - logger.ContextLogger(ctx).Info("found existing customer by email", - zap.Int64("customerId", customer.ID)) - - return &entity.CustomerCheckResponse{ - Exists: true, - Customer: customer, - Message: "Email sudah terdaftar. Silakan gunakan email lain atau login.", - }, nil - } - } - - return response, nil -} - -func (s *customerSvc) GetAllCustomers(ctx mycontext.Context, req *entity.MemberSearch) (*entity.MemberList, int, error) { - if req.Limit <= 0 { - req.Limit = 10 - } - if req.Offset < 0 { - req.Offset = 0 - } - - customers, totalCount, err := s.repo.GetAllCustomers(ctx, *req) - if err != nil { - logger.ContextLogger(ctx).Error("failed to retrieve customers", - zap.Error(err), - zap.String("search", req.Search), - ) - return nil, 0, errors.Wrap(err, "failed to get customers") - } - - return &customers, totalCount, nil -} - -func (s *customerSvc) sendRegistrationOTP( - ctx mycontext.Context, - registration *entity.MemberRegistration, -) error { - emailData := map[string]interface{}{ - "UserName": registration.Name, - "OTPCode": registration.OTP, - } - - err := s.notification.SendEmailTransactional(ctx, entity.SendEmailNotificationParam{ - Sender: "noreply@enaklo.co.id", - Recipient: registration.Email, - Subject: "Enaklo - Registration Verification Code", - TemplateName: "member_registration_otp", - TemplatePath: "templates/member_registration_otp.html", - Data: emailData, - }) - - if err != nil { - return err - } - - return nil -} - -func (s *customerSvc) VerifyOTP(ctx mycontext.Context, verificationID, otpCode string) error { - customerID, err := s.repo.VerifyOTP(ctx, verificationID, otpCode) - if err != nil { - return errors.Wrap(err, "verification failed") - } - - customer, _ := s.repo.FindByID(ctx, customerID) - - go func(customer *entity.Customer) { - newCtx := context.Background() - - defer func() { - if r := recover(); r != nil { - log.Printf("Recovered from panic in sendWelcomeEmail: %v", r) - } - }() - - s.sendWelcomeEmail(newCtx, customer) - }(customer) - - return nil -} - -func (s *customerSvc) sendWelcomeEmail( - ctx context.Context, - customer *entity.Customer, -) error { - - welcomeData := map[string]interface{}{ - "UserName": customer.Name, - "MemberID": customer.CustomerID, - "PointsName": "EnakPoint", - "PointsBalance": customer.Points, - "RedeemLink": "https://enaklo.co.id/redeem", - "CurrentDate": time.Now().Format("01-2006"), - } - - return s.notification.SendEmailTransactional(ctx, entity.SendEmailNotificationParam{ - Sender: "noreply@enaklo.co.id", - Recipient: customer.Email, - Subject: "Welcome to Enaklo Membership Program", - TemplateName: "welcome_member", - TemplatePath: "templates/welcome_member.html", - Data: welcomeData, - }) -} diff --git a/internal/services/v2/inprogress_order/in_progress_order.go b/internal/services/v2/inprogress_order/in_progress_order.go deleted file mode 100644 index 80dba79..0000000 --- a/internal/services/v2/inprogress_order/in_progress_order.go +++ /dev/null @@ -1,321 +0,0 @@ -package inprogress_order - -import ( - "context" - "database/sql" - "enaklo-pos-be/internal/common/logger" - "enaklo-pos-be/internal/common/mycontext" - order2 "enaklo-pos-be/internal/constants/order" - "enaklo-pos-be/internal/entity" - "enaklo-pos-be/internal/services/v2/order" - "fmt" - - "gorm.io/gorm" - - "github.com/pkg/errors" - "go.uber.org/zap" -) - -type InProgressOrderService interface { - Save(ctx mycontext.Context, order *entity.OrderRequest) (*entity.Order, error) - GetOrdersByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int) ([]*entity.Order, error) - GetOrderByOrderAndPartnerID(ctx mycontext.Context, partnerID int64, orderID int64) (*entity.Order, error) - AddItems(ctx mycontext.Context, orderID int64, newItems []entity.OrderItemRequest) (*entity.Order, error) -} - -type OrderRepository interface { - FindByID(ctx mycontext.Context, id int64) (*entity.Order, error) - FindByIDWithTx(ctx mycontext.Context, id int64, tx *gorm.DB) (*entity.Order, error) - CreateOrder(ctx mycontext.Context, order *entity.Order, tx *gorm.DB) (*entity.Order, error) - CreateOrderItems(ctx mycontext.Context, orderID int64, items []entity.OrderItem, tx *gorm.DB) error - GetListByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int, status string) ([]*entity.Order, error) - FindByIDAndPartnerID(ctx mycontext.Context, id int64, partnerID int64) (*entity.Order, error) - UpdateOrderTotalsWithTx(ctx mycontext.Context, trx *gorm.DB, orderID int64, amount, tax, total float64) error -} - -type OrderCalculator interface { - CalculateOrderTotals(ctx mycontext.Context, items []entity.OrderItemRequest, productDetails *entity.ProductDetails, source string, partnerID int64) (*entity.OrderCalculation, error) - ValidateOrderItems(ctx mycontext.Context, items []entity.OrderItemRequest) ([]int64, []entity.OrderItemRequest, error) -} - -type TransactionManager interface { - Begin(ctx context.Context, opts ...*sql.TxOptions) (*gorm.DB, error) - Commit(session *gorm.DB) *gorm.DB - Rollback(session *gorm.DB) *gorm.DB -} - -type inProgressOrderSvc struct { - repo OrderRepository - orderCalculator OrderCalculator - product order.ProductService - trx TransactionManager -} - -func NewInProgressOrderService(repo OrderRepository, - calculator OrderCalculator, product order.ProductService, trx TransactionManager) InProgressOrderService { - return &inProgressOrderSvc{ - repo: repo, - orderCalculator: calculator, - product: product, - trx: trx, - } -} - -func (s *inProgressOrderSvc) Save(ctx mycontext.Context, req *entity.OrderRequest) (*entity.Order, error) { - orderItems, err := s.prepareOrderItems(ctx, req.OrderItems, req.PartnerID) - if err != nil { - return nil, err - } - - orderCalculation, err := s.calculateOrderTotals(ctx, req.OrderItems, req.Source, req.PartnerID) - if err != nil { - return nil, err - } - - tx, err := s.trx.Begin(ctx) - if err != nil { - return nil, errors.Wrap(err, "failed to begin transaction") - } - defer func() { - if r := recover(); r != nil { - s.trx.Rollback(tx) - } - }() - - orderToSave := s.createOrderEntity(req, nil, orderCalculation) // Save order without items first - createdOrder, err := s.repo.CreateOrder(ctx, orderToSave, tx) - if err != nil { - s.trx.Rollback(tx) - if logger.ContextLogger(ctx) != nil { - logger.ContextLogger(ctx).Error("failed to create in-progress order", zap.Error(err), zap.Int64("partnerID", orderToSave.PartnerID)) - } - return nil, errors.Wrap(err, "failed to create in-progress order") - } - - err = s.repo.CreateOrderItems(ctx, createdOrder.ID, orderItems, tx) - if err != nil { - s.trx.Rollback(tx) - if logger.ContextLogger(ctx) != nil { - logger.ContextLogger(ctx).Error("failed to create order items", zap.Error(err), zap.Int64("orderID", createdOrder.ID)) - } - return nil, errors.Wrap(err, "failed to create order items") - } - - if err := s.trx.Commit(tx).Error; err != nil { - return nil, errors.Wrap(err, "failed to commit transaction") - } - - fullOrder, err := s.repo.FindByID(ctx, createdOrder.ID) - if err != nil { - return nil, errors.Wrap(err, "failed to fetch created order") - } - return fullOrder, nil -} - -func (s *inProgressOrderSvc) AddItems(ctx mycontext.Context, orderID int64, newItems []entity.OrderItemRequest) (*entity.Order, error) { - existingOrder, err := s.repo.FindByID(ctx, orderID) - if err != nil { - return nil, errors.Wrapf(err, "failed to fetch order %d", orderID) - } - - if existingOrder.Status != order2.Pending.String() { - return nil, errors.Errorf("cannot add items to order with status %s", existingOrder.Status) - } - - newOrderItems, err := s.prepareOrderItems(ctx, newItems, existingOrder.PartnerID) - if err != nil { - return nil, err - } - - tx, err := s.trx.Begin(ctx) - if err != nil { - return nil, errors.Wrap(err, "failed to begin transaction") - } - defer func() { - if r := recover(); r != nil { - s.trx.Rollback(tx) - } - }() - - err = s.repo.CreateOrderItems(ctx, existingOrder.ID, newOrderItems, tx) - if err != nil { - s.trx.Rollback(tx) - if logger.ContextLogger(ctx) != nil { - logger.ContextLogger(ctx).Error("failed to add order items", - zap.Error(err), - zap.Int64("orderID", existingOrder.ID)) - } - return nil, errors.Wrap(err, "failed to add order items") - } - - updatedOrder, err := s.repo.FindByIDWithTx(ctx, existingOrder.ID, tx) - if err != nil { - s.trx.Rollback(tx) - return nil, errors.Wrap(err, "failed to fetch updated order") - } - - combinedItemRequests := s.convertToOrderItemRequests(updatedOrder.OrderItems) - orderCalculation, err := s.calculateOrderTotals(ctx, combinedItemRequests, updatedOrder.Source, updatedOrder.PartnerID) - if err != nil { - s.trx.Rollback(tx) - return nil, err - } - - updatedOrder.Total = orderCalculation.Total - updatedOrder.Tax = orderCalculation.Tax - updatedOrder.Amount = orderCalculation.Subtotal - - err = s.repo.UpdateOrderTotalsWithTx(ctx, - tx, - updatedOrder.ID, - orderCalculation.Subtotal, - orderCalculation.Tax, - orderCalculation.Total) - - if err != nil { - s.trx.Rollback(tx) - if logger.ContextLogger(ctx) != nil { - logger.ContextLogger(ctx).Error("failed to update order totals", zap.Error(err), zap.Int64("orderID", updatedOrder.ID)) - } - return nil, errors.Wrap(err, "failed to update order totals") - } - - if err := s.trx.Commit(tx).Error; err != nil { - return nil, errors.Wrap(err, "failed to commit transaction") - } - - updatedOrder.OrderItems = newOrderItems - - return updatedOrder, nil -} - -func (s *inProgressOrderSvc) prepareOrderItems(ctx mycontext.Context, items []entity.OrderItemRequest, partnerID int64) ([]entity.OrderItem, error) { - productIDs, filteredItems, err := s.orderCalculator.ValidateOrderItems(ctx, items) - if err != nil { - return nil, err - } - - productDetails, err := s.product.GetProductDetails(ctx, productIDs, partnerID) - if err != nil { - if logger.ContextLogger(ctx) != nil { - logger.ContextLogger(ctx).Error("failed to get product details", zap.Error(err)) - } - return nil, err - } - - orderItems := make([]entity.OrderItem, len(filteredItems)) - for i, item := range filteredItems { - product, exists := productDetails.Products[item.ProductID] - productName := "" - if exists { - productName = product.Name - } - - orderItems[i] = entity.OrderItem{ - ItemID: item.ProductID, - ItemName: productName, - Quantity: item.Quantity, - Price: product.Price, - ItemType: product.Type, - Notes: item.Notes, - } - } - - return orderItems, nil -} - -func (s *inProgressOrderSvc) calculateOrderTotals(ctx mycontext.Context, items []entity.OrderItemRequest, source string, partnerID int64) (*entity.OrderCalculation, error) { - productIDs, _, err := s.orderCalculator.ValidateOrderItems(ctx, items) - if err != nil { - return nil, err - } - - productDetails, err := s.product.GetProductDetails(ctx, productIDs, partnerID) - if err != nil { - return nil, err - } - - return s.orderCalculator.CalculateOrderTotals(ctx, items, productDetails, source, partnerID) -} - -func (s *inProgressOrderSvc) createOrderEntity(req *entity.OrderRequest, orderItems []entity.OrderItem, calculation *entity.OrderCalculation) *entity.Order { - return &entity.Order{ - ID: req.ID, - PartnerID: req.PartnerID, - CustomerID: req.CustomerID, - CustomerName: req.CustomerName, - CreatedBy: req.CreatedBy, - OrderItems: orderItems, - TableNumber: req.TableNumber, - OrderType: req.OrderType, - Total: calculation.Total, - Tax: calculation.Tax, - Amount: calculation.Subtotal, - Status: order2.Pending.String(), - Source: req.Source, - } -} - -func (s *inProgressOrderSvc) convertToOrderItemRequests(items []entity.OrderItem) []entity.OrderItemRequest { - requests := make([]entity.OrderItemRequest, len(items)) - for i, item := range items { - requests[i] = entity.OrderItemRequest{ - ProductID: item.ItemID, - Quantity: item.Quantity, - Notes: item.Notes, - } - } - return requests -} - -func (s *inProgressOrderSvc) extractNewlyAddedItems(updatedOrder *entity.Order, existingItems []entity.OrderItem) []entity.OrderItem { - if len(existingItems) == 0 { - return updatedOrder.OrderItems - } - - existingItemMap := make(map[string]struct{}) - for _, item := range existingItems { - key := s.createItemKey(item) - existingItemMap[key] = struct{}{} - } - - newlyAdded := make([]entity.OrderItem, 0) - for _, item := range updatedOrder.OrderItems { - key := s.createItemKey(item) - if _, exists := existingItemMap[key]; !exists { - newlyAdded = append(newlyAdded, item) - } - } - - return newlyAdded -} - -func (s *inProgressOrderSvc) createItemKey(item entity.OrderItem) string { - return fmt.Sprintf("%d_%s", item.ItemID, item.Notes) -} - -func (s *inProgressOrderSvc) GetOrdersByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int) ([]*entity.Order, error) { - orders, err := s.repo.GetListByPartnerID(ctx, partnerID, limit, offset, order2.Pending.String()) - if err != nil { - logger.ContextLogger(ctx).Error("failed to get in-progress orders by partner ID", - zap.Error(err), - zap.Int64("partnerID", partnerID), - zap.Int("limit", limit), - zap.Int("offset", offset)) - return nil, errors.Wrap(err, "failed to get in-progress orders") - } - - return orders, nil -} - -func (s *inProgressOrderSvc) GetOrderByOrderAndPartnerID(ctx mycontext.Context, partnerID int64, orderID int64) (*entity.Order, error) { - orders, err := s.repo.FindByIDAndPartnerID(ctx, orderID, partnerID) - if err != nil { - logger.ContextLogger(ctx).Error("failed to get in-progress orders by partner ID", - zap.Error(err), - zap.Int64("partnerID", partnerID)) - return nil, errors.Wrap(err, "failed to get order") - } - - return orders, nil -} diff --git a/internal/services/v2/inprogress_order/in_progress_order_test.go b/internal/services/v2/inprogress_order/in_progress_order_test.go deleted file mode 100644 index 7dc09c2..0000000 --- a/internal/services/v2/inprogress_order/in_progress_order_test.go +++ /dev/null @@ -1,898 +0,0 @@ -package inprogress_order - -import ( - "context" - "database/sql" - "enaklo-pos-be/internal/common/mycontext" - order2 "enaklo-pos-be/internal/constants/order" - "enaklo-pos-be/internal/entity" - "errors" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "gorm.io/gorm" -) - -// Mock implementations -type MockOrderRepository struct { - mock.Mock -} - -func (m *MockOrderRepository) FindByID(ctx mycontext.Context, id int64) (*entity.Order, error) { - args := m.Called(ctx, id) - if args.Get(0) == nil { - return nil, args.Error(1) - } - return args.Get(0).(*entity.Order), args.Error(1) -} - -func (m *MockOrderRepository) CreateOrder(ctx mycontext.Context, order *entity.Order, tx *gorm.DB) (*entity.Order, error) { - args := m.Called(ctx, order, tx) - if args.Get(0) == nil { - return nil, args.Error(1) - } - return args.Get(0).(*entity.Order), args.Error(1) -} - -func (m *MockOrderRepository) CreateOrderItems(ctx mycontext.Context, orderID int64, items []entity.OrderItem, tx *gorm.DB) error { - args := m.Called(ctx, orderID, items, tx) - return args.Error(0) -} - -func (m *MockOrderRepository) GetListByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int, status string) ([]*entity.Order, error) { - args := m.Called(ctx, partnerID, limit, offset, status) - if args.Get(0) == nil { - return nil, args.Error(1) - } - return args.Get(0).([]*entity.Order), args.Error(1) -} - -func (m *MockOrderRepository) FindByIDAndPartnerID(ctx mycontext.Context, id int64, partnerID int64) (*entity.Order, error) { - args := m.Called(ctx, id, partnerID) - if args.Get(0) == nil { - return nil, args.Error(1) - } - return args.Get(0).(*entity.Order), args.Error(1) -} - -type MockOrderCalculator struct { - mock.Mock -} - -func (m *MockOrderCalculator) CalculateOrderTotals(ctx mycontext.Context, items []entity.OrderItemRequest, productDetails *entity.ProductDetails, source string, partnerID int64) (*entity.OrderCalculation, error) { - args := m.Called(ctx, items, productDetails, source, partnerID) - if args.Get(0) == nil { - return nil, args.Error(1) - } - return args.Get(0).(*entity.OrderCalculation), args.Error(1) -} - -func (m *MockOrderCalculator) ValidateOrderItems(ctx mycontext.Context, items []entity.OrderItemRequest) ([]int64, []entity.OrderItemRequest, error) { - args := m.Called(ctx, items) - - // Handle nil values properly - var productIDs []int64 - if args.Get(0) != nil { - productIDs = args.Get(0).([]int64) - } - - var filteredItems []entity.OrderItemRequest - if args.Get(1) != nil { - filteredItems = args.Get(1).([]entity.OrderItemRequest) - } - - return productIDs, filteredItems, args.Error(2) -} - -type MockProductService struct { - mock.Mock -} - -func (m *MockProductService) GetProductDetails(ctx mycontext.Context, productIDs []int64, partnerID int64) (*entity.ProductDetails, error) { - args := m.Called(ctx, productIDs, partnerID) - if args.Get(0) == nil { - return nil, args.Error(1) - } - return args.Get(0).(*entity.ProductDetails), args.Error(1) -} - -func (m *MockProductService) GetProductsByIDs(ctx mycontext.Context, ids []int64, partnerID int64) ([]*entity.Product, error) { - args := m.Called(ctx, ids, partnerID) - if args.Get(0) == nil { - return nil, args.Error(1) - } - return args.Get(0).([]*entity.Product), args.Error(1) -} - -type MockTransactionManager struct { - mock.Mock -} - -func (m *MockTransactionManager) Begin(ctx context.Context, opts ...*sql.TxOptions) (*gorm.DB, error) { - args := m.Called(ctx, opts) - return args.Get(0).(*gorm.DB), args.Error(1) -} - -func (m *MockTransactionManager) Commit(session *gorm.DB) *gorm.DB { - args := m.Called(session) - return args.Get(0).(*gorm.DB) -} - -func (m *MockTransactionManager) Rollback(session *gorm.DB) *gorm.DB { - args := m.Called(session) - return args.Get(0).(*gorm.DB) -} - -func TestInProgressOrderService_Save(t *testing.T) { - tests := []struct { - name string - request *entity.OrderRequest - setupMocks func(*MockOrderRepository, *MockOrderCalculator, *MockProductService) - expectedResult *entity.Order - expectedError string - }{ - { - name: "successful order creation", - request: &entity.OrderRequest{ - ID: 1, - PartnerID: 100, - CustomerID: func() *int64 { id := int64(200); return &id }(), - CustomerName: "John Doe", - CreatedBy: 300, - OrderItems: []entity.OrderItemRequest{ - {ProductID: 1, Quantity: 2, Notes: "Extra spicy"}, - {ProductID: 2, Quantity: 1, Notes: ""}, - }, - TableNumber: "A1", - OrderType: "DINE_IN", - Source: "POS", - }, - setupMocks: func(repo *MockOrderRepository, calc *MockOrderCalculator, prod *MockProductService) { - // Mock ValidateOrderItems - calc.On("ValidateOrderItems", mock.Anything, mock.Anything).Return( - []int64{1, 2}, - []entity.OrderItemRequest{ - {ProductID: 1, Quantity: 2, Notes: "Extra spicy"}, - {ProductID: 2, Quantity: 1, Notes: ""}, - }, - nil, - ) - - // Mock GetProductDetails - productDetails := &entity.ProductDetails{ - Products: map[int64]*entity.Product{ - 1: {ID: 1, Name: "Burger", Price: 10.0, Type: "PRODUCT"}, - 2: {ID: 2, Name: "Fries", Price: 5.0, Type: "PRODUCT"}, - }, - } - prod.On("GetProductDetails", mock.Anything, []int64{1, 2}, int64(100)).Return(productDetails, nil) - - // Mock CalculateOrderTotals - calc.On("CalculateOrderTotals", mock.Anything, mock.Anything, productDetails, "POS", int64(100)).Return( - &entity.OrderCalculation{ - Subtotal: 25.0, - Tax: 2.5, - Total: 27.5, - }, - nil, - ) - - // Mock CreateOrder (returns order without items) - createdOrder := &entity.Order{ - ID: 1, - PartnerID: 100, - CustomerID: func() *int64 { id := int64(200); return &id }(), - CustomerName: "John Doe", - CreatedBy: 300, - TableNumber: "A1", - OrderType: "DINE_IN", - Total: 27.5, - Tax: 2.5, - Amount: 25.0, - Status: order2.Pending.String(), - Source: "POS", - } - repo.On("CreateOrder", mock.Anything, mock.Anything).Return(createdOrder, nil) - - // Mock CreateOrderItems - repo.On("CreateOrderItems", mock.Anything, int64(1), mock.Anything).Return(nil) - - // Mock FindByID (returns full order with items) - expectedOrder := &entity.Order{ - ID: 1, - PartnerID: 100, - CustomerID: func() *int64 { id := int64(200); return &id }(), - CustomerName: "John Doe", - CreatedBy: 300, - OrderItems: []entity.OrderItem{ - {ItemID: 1, ItemName: "Burger", Quantity: 2, Price: 10.0, ItemType: "PRODUCT", Notes: "Extra spicy"}, - {ItemID: 2, ItemName: "Fries", Quantity: 1, Price: 5.0, ItemType: "PRODUCT", Notes: ""}, - }, - TableNumber: "A1", - OrderType: "DINE_IN", - Total: 27.5, - Tax: 2.5, - Amount: 25.0, - Status: order2.Pending.String(), - Source: "POS", - } - repo.On("FindByID", mock.Anything, int64(1)).Return(expectedOrder, nil) - }, - expectedResult: &entity.Order{ - ID: 1, - PartnerID: 100, - CustomerID: func() *int64 { id := int64(200); return &id }(), - CustomerName: "John Doe", - CreatedBy: 300, - OrderItems: []entity.OrderItem{ - {ItemID: 1, ItemName: "Burger", Quantity: 2, Price: 10.0, ItemType: "PRODUCT", Notes: "Extra spicy"}, - {ItemID: 2, ItemName: "Fries", Quantity: 1, Price: 5.0, ItemType: "PRODUCT", Notes: ""}, - }, - TableNumber: "A1", - OrderType: "DINE_IN", - Total: 27.5, - Tax: 2.5, - Amount: 25.0, - Status: order2.Pending.String(), - Source: "POS", - }, - expectedError: "", - }, - { - name: "validation error", - request: &entity.OrderRequest{ - PartnerID: 100, - OrderItems: []entity.OrderItemRequest{ - {ProductID: 1, Quantity: 0}, // Invalid quantity - }, - }, - setupMocks: func(repo *MockOrderRepository, calc *MockOrderCalculator, prod *MockProductService) { - calc.On("ValidateOrderItems", mock.Anything, mock.Anything).Return( - nil, nil, errors.New("invalid quantity"), - ) - }, - expectedResult: nil, - expectedError: "invalid quantity", - }, - { - name: "product details error", - request: &entity.OrderRequest{ - PartnerID: 100, - OrderItems: []entity.OrderItemRequest{ - {ProductID: 1, Quantity: 1}, - }, - }, - setupMocks: func(repo *MockOrderRepository, calc *MockOrderCalculator, prod *MockProductService) { - calc.On("ValidateOrderItems", mock.Anything, mock.Anything).Return( - []int64{1}, - []entity.OrderItemRequest{{ProductID: 1, Quantity: 1}}, - nil, - ) - prod.On("GetProductDetails", mock.Anything, []int64{1}, int64(100)).Return(nil, errors.New("product not found")) - }, - expectedResult: nil, - expectedError: "product not found", - }, - { - name: "calculation error", - request: &entity.OrderRequest{ - PartnerID: 100, - OrderItems: []entity.OrderItemRequest{ - {ProductID: 1, Quantity: 1}, - }, - }, - setupMocks: func(repo *MockOrderRepository, calc *MockOrderCalculator, prod *MockProductService) { - calc.On("ValidateOrderItems", mock.Anything, mock.Anything).Return( - []int64{1}, - []entity.OrderItemRequest{{ProductID: 1, Quantity: 1}}, - nil, - ) - productDetails := &entity.ProductDetails{ - Products: map[int64]*entity.Product{ - 1: {ID: 1, Name: "Burger", Price: 10.0, Type: "PRODUCT"}, - }, - } - prod.On("GetProductDetails", mock.Anything, []int64{1}, int64(100)).Return(productDetails, nil) - calc.On("CalculateOrderTotals", mock.Anything, mock.Anything, productDetails, "", int64(100)).Return( - nil, errors.New("calculation failed"), - ) - }, - expectedResult: nil, - expectedError: "calculation failed", - }, - { - name: "repository error", - request: &entity.OrderRequest{ - PartnerID: 100, - OrderItems: []entity.OrderItemRequest{ - {ProductID: 1, Quantity: 1}, - }, - }, - setupMocks: func(repo *MockOrderRepository, calc *MockOrderCalculator, prod *MockProductService) { - calc.On("ValidateOrderItems", mock.Anything, mock.Anything).Return( - []int64{1}, - []entity.OrderItemRequest{{ProductID: 1, Quantity: 1}}, - nil, - ) - productDetails := &entity.ProductDetails{ - Products: map[int64]*entity.Product{ - 1: {ID: 1, Name: "Burger", Price: 10.0, Type: "PRODUCT"}, - }, - } - prod.On("GetProductDetails", mock.Anything, []int64{1}, int64(100)).Return(productDetails, nil) - calc.On("CalculateOrderTotals", mock.Anything, mock.Anything, productDetails, "", int64(100)).Return( - &entity.OrderCalculation{Subtotal: 10.0, Tax: 1.0, Total: 11.0}, - nil, - ) - repo.On("CreateOrder", mock.Anything, mock.Anything).Return(nil, errors.New("database error")) - }, - expectedResult: nil, - expectedError: "failed to create in-progress order: database error", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Setup mocks - mockRepo := &MockOrderRepository{} - mockCalc := &MockOrderCalculator{} - mockProd := &MockProductService{} - - if tt.setupMocks != nil { - tt.setupMocks(mockRepo, mockCalc, mockProd) - } - - // Create service - service := NewInProgressOrderService(mockRepo, mockCalc, mockProd) - - // Execute - ctx := mycontext.NewContext(context.Background()) - result, err := service.Save(ctx, tt.request) - - // Assert - if tt.expectedError != "" { - assert.Error(t, err) - assert.Contains(t, err.Error(), tt.expectedError) - assert.Nil(t, result) - } else { - assert.NoError(t, err) - assert.NotNil(t, result) - assert.Equal(t, tt.expectedResult.ID, result.ID) - assert.Equal(t, tt.expectedResult.PartnerID, result.PartnerID) - assert.Equal(t, tt.expectedResult.Status, result.Status) - assert.Equal(t, tt.expectedResult.Total, result.Total) - assert.Len(t, result.OrderItems, len(tt.expectedResult.OrderItems)) - } - - // Verify all mocks were called as expected - mockRepo.AssertExpectations(t) - mockCalc.AssertExpectations(t) - mockProd.AssertExpectations(t) - }) - } -} - -func TestInProgressOrderService_AddItems(t *testing.T) { - tests := []struct { - name string - orderID int64 - newItems []entity.OrderItemRequest - setupMocks func(*MockOrderRepository, *MockOrderCalculator, *MockProductService) - expectedResult *entity.Order - expectedError string - }{ - { - name: "successful add items to pending order", - orderID: 1, - newItems: []entity.OrderItemRequest{ - {ProductID: 3, Quantity: 1, Notes: "No onions"}, - {ProductID: 4, Quantity: 2, Notes: ""}, - }, - setupMocks: func(repo *MockOrderRepository, calc *MockOrderCalculator, prod *MockProductService) { - // Mock existing order - existingOrder := &entity.Order{ - ID: 1, - PartnerID: 100, - Status: order2.Pending.String(), - OrderItems: []entity.OrderItem{ - {ItemID: 1, ItemName: "Burger", Quantity: 2, Price: 10.0, ItemType: "PRODUCT", Notes: "Extra spicy"}, - }, - Source: "POS", - } - repo.On("FindByID", mock.Anything, int64(1)).Return(existingOrder, nil).Once() - - // Mock ValidateOrderItems for new items - calc.On("ValidateOrderItems", mock.Anything, mock.Anything).Return( - []int64{3, 4}, - []entity.OrderItemRequest{ - {ProductID: 3, Quantity: 1, Notes: "No onions"}, - {ProductID: 4, Quantity: 2, Notes: ""}, - }, - nil, - ) - - // Mock GetProductDetails - productDetails := &entity.ProductDetails{ - Products: map[int64]*entity.Product{ - 3: {ID: 3, Name: "Salad", Price: 8.0, Type: "PRODUCT"}, - 4: {ID: 4, Name: "Drink", Price: 3.0, Type: "PRODUCT"}, - }, - } - prod.On("GetProductDetails", mock.Anything, []int64{3, 4}, int64(100)).Return(productDetails, nil) - - // Mock CreateOrderItems - repo.On("CreateOrderItems", mock.Anything, int64(1), mock.Anything).Return(nil) - - // Mock FindByID (returns updated order with all items) - updatedOrder := &entity.Order{ - ID: 1, - PartnerID: 100, - Status: order2.Pending.String(), - OrderItems: []entity.OrderItem{ - {ItemID: 1, ItemName: "Burger", Quantity: 2, Price: 10.0, ItemType: "PRODUCT", Notes: "Extra spicy"}, - {ItemID: 3, ItemName: "Salad", Quantity: 1, Price: 8.0, ItemType: "PRODUCT", Notes: "No onions"}, - {ItemID: 4, ItemName: "Drink", Quantity: 2, Price: 3.0, ItemType: "PRODUCT", Notes: ""}, - }, - Total: 40.7, - Tax: 3.7, - Amount: 37.0, - Source: "POS", - } - repo.On("FindByID", mock.Anything, int64(1)).Return(updatedOrder, nil).Once() - - // Mock CalculateOrderTotals for combined items - calc.On("CalculateOrderTotals", mock.Anything, mock.Anything, mock.Anything, "POS", int64(100)).Return( - &entity.OrderCalculation{ - Subtotal: 37.0, - Tax: 3.7, - Total: 40.7, - }, - nil, - ) - - // Mock CreateOrder for updating totals - repo.On("CreateOrder", mock.Anything, mock.Anything).Return(updatedOrder, nil) - }, - expectedResult: &entity.Order{ - ID: 1, - PartnerID: 100, - Status: order2.Pending.String(), - OrderItems: []entity.OrderItem{ - {ItemID: 3, ItemName: "Salad", Quantity: 1, Price: 8.0, ItemType: "PRODUCT", Notes: "No onions"}, - {ItemID: 4, ItemName: "Drink", Quantity: 2, Price: 3.0, ItemType: "PRODUCT", Notes: ""}, - }, - Total: 40.7, - Tax: 3.7, - Amount: 37.0, - Source: "POS", - }, - expectedError: "", - }, - { - name: "order not found", - orderID: 999, - newItems: []entity.OrderItemRequest{ - {ProductID: 1, Quantity: 1}, - }, - setupMocks: func(repo *MockOrderRepository, calc *MockOrderCalculator, prod *MockProductService) { - repo.On("FindByID", mock.Anything, int64(999)).Return(nil, errors.New("order not found")) - }, - expectedResult: nil, - expectedError: "failed to fetch order 999: order not found", - }, - { - name: "order not in pending status", - orderID: 1, - newItems: []entity.OrderItemRequest{ - {ProductID: 1, Quantity: 1}, - }, - setupMocks: func(repo *MockOrderRepository, calc *MockOrderCalculator, prod *MockProductService) { - existingOrder := &entity.Order{ - ID: 1, - Status: order2.Paid.String(), - } - repo.On("FindByID", mock.Anything, int64(1)).Return(existingOrder, nil) - }, - expectedResult: nil, - expectedError: "cannot add items to order with status PAID", - }, - { - name: "validation error for new items", - orderID: 1, - newItems: []entity.OrderItemRequest{ - {ProductID: 1, Quantity: 0}, // Invalid quantity - }, - setupMocks: func(repo *MockOrderRepository, calc *MockOrderCalculator, prod *MockProductService) { - existingOrder := &entity.Order{ - ID: 1, - PartnerID: 100, - Status: order2.Pending.String(), - OrderItems: []entity.OrderItem{ - {ItemID: 1, ItemName: "Burger", Quantity: 2, Price: 10.0, ItemType: "PRODUCT", Notes: "Extra spicy"}, - }, - } - repo.On("FindByID", mock.Anything, int64(1)).Return(existingOrder, nil) - calc.On("ValidateOrderItems", mock.Anything, mock.Anything).Return( - nil, nil, errors.New("invalid quantity"), - ) - }, - expectedResult: nil, - expectedError: "invalid quantity", - }, - { - name: "product details error", - orderID: 1, - newItems: []entity.OrderItemRequest{ - {ProductID: 1, Quantity: 1}, - }, - setupMocks: func(repo *MockOrderRepository, calc *MockOrderCalculator, prod *MockProductService) { - existingOrder := &entity.Order{ - ID: 1, - PartnerID: 100, - Status: order2.Pending.String(), - OrderItems: []entity.OrderItem{ - {ItemID: 1, ItemName: "Burger", Quantity: 2, Price: 10.0, ItemType: "PRODUCT", Notes: "Extra spicy"}, - }, - } - repo.On("FindByID", mock.Anything, int64(1)).Return(existingOrder, nil) - calc.On("ValidateOrderItems", mock.Anything, mock.Anything).Return( - []int64{1}, - []entity.OrderItemRequest{{ProductID: 1, Quantity: 1}}, - nil, - ) - prod.On("GetProductDetails", mock.Anything, []int64{1}, int64(100)).Return(nil, errors.New("product not found")) - }, - expectedResult: nil, - expectedError: "product not found", - }, - { - name: "calculation error", - orderID: 1, - newItems: []entity.OrderItemRequest{ - {ProductID: 1, Quantity: 1}, - }, - setupMocks: func(repo *MockOrderRepository, calc *MockOrderCalculator, prod *MockProductService) { - existingOrder := &entity.Order{ - ID: 1, - PartnerID: 100, - Status: order2.Pending.String(), - OrderItems: []entity.OrderItem{ - {ItemID: 1, ItemName: "Burger", Quantity: 2, Price: 10.0, ItemType: "PRODUCT", Notes: "Extra spicy"}, - }, - Source: "POS", - } - repo.On("FindByID", mock.Anything, int64(1)).Return(existingOrder, nil) - calc.On("ValidateOrderItems", mock.Anything, mock.Anything).Return( - []int64{1}, - []entity.OrderItemRequest{{ProductID: 1, Quantity: 1}}, - nil, - ) - productDetails := &entity.ProductDetails{ - Products: map[int64]*entity.Product{ - 1: {ID: 1, Name: "Burger", Price: 10.0, Type: "PRODUCT"}, - }, - } - prod.On("GetProductDetails", mock.Anything, []int64{1}, int64(100)).Return(productDetails, nil) - calc.On("CalculateOrderTotals", mock.Anything, mock.Anything, mock.Anything, "POS", int64(100)).Return( - nil, errors.New("calculation failed"), - ) - }, - expectedResult: nil, - expectedError: "calculation failed", - }, - { - name: "repository update error", - orderID: 1, - newItems: []entity.OrderItemRequest{ - {ProductID: 1, Quantity: 1}, - }, - setupMocks: func(repo *MockOrderRepository, calc *MockOrderCalculator, prod *MockProductService) { - existingOrder := &entity.Order{ - ID: 1, - PartnerID: 100, - Status: order2.Pending.String(), - OrderItems: []entity.OrderItem{ - {ItemID: 1, ItemName: "Burger", Quantity: 2, Price: 10.0, ItemType: "PRODUCT", Notes: "Extra spicy"}, - }, - Source: "POS", - } - repo.On("FindByID", mock.Anything, int64(1)).Return(existingOrder, nil) - calc.On("ValidateOrderItems", mock.Anything, mock.Anything).Return( - []int64{1}, - []entity.OrderItemRequest{{ProductID: 1, Quantity: 1}}, - nil, - ) - productDetails := &entity.ProductDetails{ - Products: map[int64]*entity.Product{ - 1: {ID: 1, Name: "Burger", Price: 10.0, Type: "PRODUCT"}, - }, - } - prod.On("GetProductDetails", mock.Anything, []int64{1}, int64(100)).Return(productDetails, nil) - calc.On("CalculateOrderTotals", mock.Anything, mock.Anything, mock.Anything, "POS", int64(100)).Return( - &entity.OrderCalculation{Subtotal: 30.0, Tax: 3.0, Total: 33.0}, - nil, - ) - repo.On("CreateOrderItems", mock.Anything, mock.Anything, mock.Anything).Return(errors.New("database error")) - }, - expectedResult: nil, - expectedError: "failed to add order items: database error", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Setup mocks - mockRepo := &MockOrderRepository{} - mockCalc := &MockOrderCalculator{} - mockProd := &MockProductService{} - - if tt.setupMocks != nil { - tt.setupMocks(mockRepo, mockCalc, mockProd) - } - - // Create service - service := NewInProgressOrderService(mockRepo, mockCalc, mockProd) - - // Execute - ctx := mycontext.NewContext(context.Background()) - result, err := service.AddItems(ctx, tt.orderID, tt.newItems) - - // Assert - if tt.expectedError != "" { - assert.Error(t, err) - assert.Contains(t, err.Error(), tt.expectedError) - assert.Nil(t, result) - } else { - assert.NoError(t, err) - assert.NotNil(t, result) - assert.Equal(t, tt.expectedResult.ID, result.ID) - assert.Equal(t, tt.expectedResult.PartnerID, result.PartnerID) - assert.Equal(t, tt.expectedResult.Status, result.Status) - assert.Equal(t, tt.expectedResult.Total, result.Total) - // Should only return newly added items - assert.Len(t, result.OrderItems, len(tt.expectedResult.OrderItems)) - } - - // Verify all mocks were called as expected - mockRepo.AssertExpectations(t) - mockCalc.AssertExpectations(t) - mockProd.AssertExpectations(t) - }) - } -} - -func TestInProgressOrderService_HelperMethods(t *testing.T) { - service := &inProgressOrderSvc{} - - t.Run("convertToOrderItemRequests", func(t *testing.T) { - items := []entity.OrderItem{ - {ItemID: 1, Quantity: 2, Notes: "Extra spicy"}, - {ItemID: 2, Quantity: 1, Notes: ""}, - } - - result := service.convertToOrderItemRequests(items) - - assert.Len(t, result, 2) - assert.Equal(t, int64(1), result[0].ProductID) - assert.Equal(t, 2, result[0].Quantity) - assert.Equal(t, "Extra spicy", result[0].Notes) - assert.Equal(t, int64(2), result[1].ProductID) - assert.Equal(t, 1, result[1].Quantity) - assert.Equal(t, "", result[1].Notes) - }) - - t.Run("createItemKey", func(t *testing.T) { - item := entity.OrderItem{ItemID: 1, Notes: "Extra spicy"} - key := service.createItemKey(item) - assert.Equal(t, "1_Extra spicy", key) - - item2 := entity.OrderItem{ItemID: 2, Notes: ""} - key2 := service.createItemKey(item2) - assert.Equal(t, "2_", key2) - }) - - t.Run("extractNewlyAddedItems", func(t *testing.T) { - existingItems := []entity.OrderItem{ - {ItemID: 1, Notes: "Extra spicy"}, - {ItemID: 2, Notes: ""}, - } - - updatedOrder := &entity.Order{ - OrderItems: []entity.OrderItem{ - {ItemID: 1, Notes: "Extra spicy"}, - {ItemID: 2, Notes: ""}, - {ItemID: 3, Notes: "No onions"}, - {ItemID: 4, Notes: ""}, - }, - } - - result := service.extractNewlyAddedItems(updatedOrder, existingItems) - - assert.Len(t, result, 2) - assert.Equal(t, int64(3), result[0].ItemID) - assert.Equal(t, "No onions", result[0].Notes) - assert.Equal(t, int64(4), result[1].ItemID) - assert.Equal(t, "", result[1].Notes) - }) - - t.Run("extractNewlyAddedItems with no existing items", func(t *testing.T) { - updatedOrder := &entity.Order{ - OrderItems: []entity.OrderItem{ - {ItemID: 1, Notes: "Extra spicy"}, - {ItemID: 2, Notes: ""}, - }, - } - - result := service.extractNewlyAddedItems(updatedOrder, []entity.OrderItem{}) - - assert.Len(t, result, 2) - assert.Equal(t, int64(1), result[0].ItemID) - assert.Equal(t, int64(2), result[1].ItemID) - }) -} - -func TestSave_WithTransaction(t *testing.T) { - // Setup - mockRepo := new(MockOrderRepository) - mockCalculator := new(MockOrderCalculator) - mockProduct := new(MockProductService) - mockTrx := new(MockTransactionManager) - - service := NewInProgressOrderService(mockRepo, mockCalculator, mockProduct, mockTrx) - - ctx := mycontext.NewContext(context.Background()) - req := &entity.OrderRequest{ - PartnerID: 1, - OrderItems: []entity.OrderItemRequest{ - {ProductID: 1, Quantity: 2}, - }, - Source: "pos", - } - - // Mock transaction - mockTx := &gorm.DB{} - mockTrx.On("Begin", ctx, mock.Anything).Return(mockTx, nil) - mockTrx.On("Commit", mockTx).Return(mockTx) - mockTrx.On("Rollback", mockTx).Return(mockTx) - - // Mock calculator - productIDs := []int64{1} - filteredItems := []entity.OrderItemRequest{{ProductID: 1, Quantity: 2}} - mockCalculator.On("ValidateOrderItems", ctx, req.OrderItems).Return(productIDs, filteredItems, nil) - - // Mock product service - productDetails := &entity.ProductDetails{ - Products: map[int64]*entity.Product{ - 1: {ID: 1, Name: "Test Product", Price: 10.0}, - }, - } - mockProduct.On("GetProductDetails", ctx, productIDs, req.PartnerID).Return(productDetails, nil) - - // Mock calculation - calculation := &entity.OrderCalculation{ - Subtotal: 20.0, - Tax: 2.0, - Total: 22.0, - } - mockCalculator.On("CalculateOrderTotals", ctx, req.OrderItems, productDetails, req.Source, req.PartnerID).Return(calculation, nil) - - // Mock repository calls - createdOrder := &entity.Order{ID: 1, PartnerID: 1} - mockRepo.On("CreateOrder", ctx, mock.AnythingOfType("*entity.Order"), mockTx).Return(createdOrder, nil) - mockRepo.On("CreateOrderItems", ctx, int64(1), mock.AnythingOfType("[]entity.OrderItem"), mockTx).Return(nil) - - fullOrder := &entity.Order{ID: 1, PartnerID: 1, OrderItems: []entity.OrderItem{}} - mockRepo.On("FindByID", ctx, int64(1)).Return(fullOrder, nil) - - // Execute - result, err := service.Save(ctx, req) - - // Assert - assert.NoError(t, err) - assert.NotNil(t, result) - assert.Equal(t, int64(1), result.ID) - - // Verify all mocks were called - mockTrx.AssertExpectations(t) - mockRepo.AssertExpectations(t) - mockCalculator.AssertExpectations(t) - mockProduct.AssertExpectations(t) -} - -func TestAddItems_WithTransaction(t *testing.T) { - // Setup - mockRepo := new(MockOrderRepository) - mockCalculator := new(MockOrderCalculator) - mockProduct := new(MockProductService) - mockTrx := new(MockTransactionManager) - - service := NewInProgressOrderService(mockRepo, mockCalculator, mockProduct, mockTrx) - - ctx := mycontext.NewContext(context.Background()) - orderID := int64(1) - newItems := []entity.OrderItemRequest{ - {ProductID: 2, Quantity: 1}, - } - - // Mock existing order - existingOrder := &entity.Order{ - ID: orderID, - Status: "pending", - OrderItems: []entity.OrderItem{ - {ItemID: 1, Quantity: 2}, - }, - } - mockRepo.On("FindByID", ctx, orderID).Return(existingOrder, nil) - - // Mock transaction - mockTx := &gorm.DB{} - mockTrx.On("Begin", ctx, mock.Anything).Return(mockTx, nil) - mockTrx.On("Commit", mockTx).Return(mockTx) - mockTrx.On("Rollback", mockTx).Return(mockTx) - - // Mock calculator - productIDs := []int64{2} - filteredItems := []entity.OrderItemRequest{{ProductID: 2, Quantity: 1}} - mockCalculator.On("ValidateOrderItems", ctx, newItems).Return(productIDs, filteredItems, nil) - - // Mock product service - productDetails := &entity.ProductDetails{ - Products: map[int64]*entity.Product{ - 2: {ID: 2, Name: "New Product", Price: 15.0}, - }, - } - mockProduct.On("GetProductDetails", ctx, productIDs, existingOrder.PartnerID).Return(productDetails, nil) - - // Mock repository calls - mockRepo.On("CreateOrderItems", ctx, orderID, mock.AnythingOfType("[]entity.OrderItem"), mockTx).Return(nil) - - updatedOrder := &entity.Order{ - ID: orderID, - Status: "pending", - OrderItems: []entity.OrderItem{ - {ItemID: 1, Quantity: 2}, - {ItemID: 2, Quantity: 1}, - }, - } - mockRepo.On("FindByID", ctx, orderID).Return(updatedOrder, nil) - - // Mock calculation for updated totals - combinedItems := []entity.OrderItemRequest{ - {ProductID: 1, Quantity: 2}, - {ProductID: 2, Quantity: 1}, - } - updatedCalculation := &entity.OrderCalculation{ - Subtotal: 35.0, - Tax: 3.5, - Total: 38.5, - } - mockCalculator.On("CalculateOrderTotals", ctx, combinedItems, productDetails, updatedOrder.Source, updatedOrder.PartnerID).Return(updatedCalculation, nil) - - updatedOrderWithTotals := &entity.Order{ - ID: orderID, - Status: "pending", - Total: 38.5, - Tax: 3.5, - Amount: 35.0, - OrderItems: []entity.OrderItem{ - {ItemID: 1, Quantity: 2}, - {ItemID: 2, Quantity: 1}, - }, - } - mockRepo.On("CreateOrder", ctx, mock.AnythingOfType("*entity.Order"), mockTx).Return(updatedOrderWithTotals, nil) - - // Execute - result, err := service.AddItems(ctx, orderID, newItems) - - // Assert - assert.NoError(t, err) - assert.NotNil(t, result) - assert.Equal(t, orderID, result.ID) - - // Verify all mocks were called - mockTrx.AssertExpectations(t) - mockRepo.AssertExpectations(t) - mockCalculator.AssertExpectations(t) - mockProduct.AssertExpectations(t) -} diff --git a/internal/services/v2/member/member.go b/internal/services/v2/member/member.go deleted file mode 100644 index 363b41e..0000000 --- a/internal/services/v2/member/member.go +++ /dev/null @@ -1 +0,0 @@ -package member diff --git a/internal/services/v2/order/advanced_order_management.go b/internal/services/v2/order/advanced_order_management.go deleted file mode 100644 index 6557a4a..0000000 --- a/internal/services/v2/order/advanced_order_management.go +++ /dev/null @@ -1,481 +0,0 @@ -package order - -import ( - "enaklo-pos-be/internal/common/logger" - "enaklo-pos-be/internal/common/mycontext" - "enaklo-pos-be/internal/entity" - "fmt" - - "github.com/pkg/errors" - "go.uber.org/zap" -) - -func (s *orderSvc) PartialRefundRequest(ctx mycontext.Context, partnerID, orderID int64, reason string, items []entity.PartialRefundItem) error { - order, err := s.repo.FindByIDAndPartnerID(ctx, orderID, partnerID) - if err != nil { - logger.ContextLogger(ctx).Error("failed to find order for partial refund", zap.Error(err)) - return err - } - - if order.Status != "PAID" && order.Status != "PARTIAL" { - return errors.New("only paid order can be partially refunded") - } - - refundedAmount := 0.0 - orderItemMap := make(map[int64]*entity.OrderItem) - - for _, item := range order.OrderItems { - orderItemMap[item.ID] = &item - } - - for _, refundItem := range items { - orderItem, exists := orderItemMap[refundItem.OrderItemID] - if !exists { - return errors.New(fmt.Sprintf("order item %d not found", refundItem.OrderItemID)) - } - - if refundItem.Quantity > orderItem.Quantity { - return errors.New(fmt.Sprintf("refund quantity %d exceeds available quantity %d for item %d", - refundItem.Quantity, orderItem.Quantity, refundItem.OrderItemID)) - } - - refundedAmount += orderItem.Price * float64(refundItem.Quantity) - } - - for _, refundItem := range items { - orderItem := orderItemMap[refundItem.OrderItemID] - newQuantity := orderItem.Quantity - refundItem.Quantity - - if newQuantity == 0 { - err = s.repo.UpdateOrderItem(ctx, refundItem.OrderItemID, 0) - } else { - err = s.repo.UpdateOrderItem(ctx, refundItem.OrderItemID, newQuantity) - } - - if err != nil { - logger.ContextLogger(ctx).Error("failed to update order item", zap.Error(err)) - return err - } - } - - remainingAmount := order.Amount - refundedAmount - remainingTax := (remainingAmount / order.Amount) * order.Tax - remainingTotal := remainingAmount + remainingTax - - err = s.repo.UpdateOrderTotals(ctx, orderID, remainingAmount, remainingTax, remainingTotal) - if err != nil { - logger.ContextLogger(ctx).Error("failed to update order totals", zap.Error(err)) - return err - } - - newStatus := "PARTIAL" - if remainingAmount <= 0 { - newStatus = "REFUNDED" - } - - err = s.repo.UpdateOrder(ctx, orderID, newStatus, reason) - if err != nil { - logger.ContextLogger(ctx).Error("failed to update order status", zap.Error(err)) - return err - } - - refundTransaction, err := s.createRefundTransaction(ctx, order, reason) - if err != nil { - logger.ContextLogger(ctx).Error("failed to create refund transaction", zap.Error(err)) - return err - } - - refundTransaction.Amount = -refundedAmount - _, err = s.transaction.Create(ctx, refundTransaction) - if err != nil { - logger.ContextLogger(ctx).Error("failed to update refund transaction", zap.Error(err)) - return err - } - - logger.ContextLogger(ctx).Info("partial refund processed successfully", - zap.Int64("orderID", orderID), - zap.String("reason", reason), - zap.Float64("refundedAmount", refundedAmount), - zap.String("refundTransactionID", refundTransaction.ID)) - - return nil -} - -// VoidOrderRequest handles voiding orders (for ongoing orders) or specific items -func (s *orderSvc) VoidOrderRequest(ctx mycontext.Context, partnerID, orderID int64, reason string, voidType string, items []entity.VoidItem) error { - order, err := s.repo.FindByIDAndPartnerID(ctx, orderID, partnerID) - if err != nil { - logger.ContextLogger(ctx).Error("failed to find order for void", zap.Error(err)) - return err - } - - if order.Status != "NEW" && order.Status != "PENDING" { - return errors.New("only new or pending orders can be voided") - } - - if voidType == "ALL" { - for _, orderItem := range order.OrderItems { - if orderItem.Status == "ACTIVE" && orderItem.Quantity > 0 { - voidedItem := &entity.OrderItem{ - OrderID: orderID, - ItemID: orderItem.ItemID, - ItemType: orderItem.ItemType, - Price: orderItem.Price, - Quantity: orderItem.Quantity, - Status: "VOIDED", - CreatedBy: orderItem.CreatedBy, - ItemName: orderItem.ItemName, - Notes: reason, - } - - err = s.repo.CreateOrderItem(ctx, orderID, voidedItem) - if err != nil { - logger.ContextLogger(ctx).Error("failed to create voided order item", zap.Error(err)) - return err - } - } - } - - err = s.repo.UpdateOrder(ctx, orderID, "VOIDED", reason) - if err != nil { - logger.ContextLogger(ctx).Error("failed to void order", zap.Error(err)) - return err - } - - err = s.repo.UpdateOrderTotals(ctx, orderID, 0, 0, 0) - if err != nil { - logger.ContextLogger(ctx).Error("failed to update order totals", zap.Error(err)) - return err - } - - } else if voidType == "ITEM" { - orderItemMap := make(map[int64]*entity.OrderItem) - for i := range order.OrderItems { - orderItemMap[order.OrderItems[i].ID] = &order.OrderItems[i] - } - - for _, voidItem := range items { - orderItem, exists := orderItemMap[voidItem.OrderItemID] - if !exists { - return errors.New(fmt.Sprintf("order item %d not found", voidItem.OrderItemID)) - } - - if orderItem.Status != "ACTIVE" { - return errors.New(fmt.Sprintf("order item %d is not active", voidItem.OrderItemID)) - } - - if voidItem.Quantity > orderItem.Quantity { - return errors.New(fmt.Sprintf("void quantity %d exceeds available quantity %d for item %d", - voidItem.Quantity, orderItem.Quantity, voidItem.OrderItemID)) - } - } - - for _, voidItem := range items { - orderItem := orderItemMap[voidItem.OrderItemID] - - voidedItem := &entity.OrderItem{ - OrderID: orderID, - ItemID: orderItem.ItemID, - ItemType: orderItem.ItemType, - Price: orderItem.Price, - Quantity: voidItem.Quantity, - Status: "VOIDED", - CreatedBy: orderItem.CreatedBy, - ItemName: orderItem.ItemName, - Notes: reason, - } - - err = s.repo.CreateOrderItem(ctx, orderID, voidedItem) - if err != nil { - logger.ContextLogger(ctx).Error("failed to create voided order item", zap.Error(err)) - return err - } - newQuantity := orderItem.Quantity - voidItem.Quantity - - if newQuantity > 0 { - err = s.repo.UpdateOrderItem(ctx, voidItem.OrderItemID, newQuantity) - if err != nil { - logger.ContextLogger(ctx).Error("failed to update order item", zap.Error(err)) - return err - } - } else { - err = s.repo.DeleteOrderItem(ctx, voidItem.OrderItemID) - if err != nil { - logger.ContextLogger(ctx).Error("failed to delete order item", zap.Error(err)) - return err - } - } - } - - updatedOrder, err := s.repo.FindByIDAndPartnerID(ctx, orderID, partnerID) - if err != nil { - logger.ContextLogger(ctx).Error("failed to fetch updated order for recalculation", zap.Error(err)) - return err - } - - var activeItems []entity.OrderItemRequest - for _, item := range updatedOrder.OrderItems { - if item.Status == "ACTIVE" && item.Quantity > 0 { - activeItems = append(activeItems, entity.OrderItemRequest{ - ProductID: item.ItemID, - Quantity: item.Quantity, - Notes: item.Notes, - }) - } - } - - if len(activeItems) > 0 { - productIDs, _, err := s.ValidateOrderItems(ctx, activeItems) - if err != nil { - logger.ContextLogger(ctx).Error("failed to validate order items for recalculation", zap.Error(err)) - return err - } - - productDetails, err := s.product.GetProductDetails(ctx, productIDs, partnerID) - if err != nil { - logger.ContextLogger(ctx).Error("failed to get product details for recalculation", zap.Error(err)) - return err - } - - orderCalculation, err := s.CalculateOrderTotals(ctx, activeItems, productDetails, order.Source, partnerID) - if err != nil { - logger.ContextLogger(ctx).Error("failed to calculate order totals", zap.Error(err)) - return err - } - - // Update order totals - err = s.repo.UpdateOrderTotals(ctx, orderID, orderCalculation.Subtotal, orderCalculation.Tax, orderCalculation.Total) - if err != nil { - logger.ContextLogger(ctx).Error("failed to update order totals", zap.Error(err)) - return err - } - - // Update order status based on remaining amount - newStatus := "PENDING" - if orderCalculation.Subtotal <= 0 { - newStatus = "CANCELED" - } - - err = s.repo.UpdateOrder(ctx, orderID, newStatus, reason) - if err != nil { - logger.ContextLogger(ctx).Error("failed to update order status", zap.Error(err)) - return err - } - } else { - // No active items left, cancel the order - err = s.repo.UpdateOrderTotals(ctx, orderID, 0, 0, 0) - if err != nil { - logger.ContextLogger(ctx).Error("failed to update order totals", zap.Error(err)) - return err - } - - err = s.repo.UpdateOrder(ctx, orderID, "CANCELED", reason) - if err != nil { - logger.ContextLogger(ctx).Error("failed to update order status", zap.Error(err)) - return err - } - } - } - - logger.ContextLogger(ctx).Info("order voided successfully", - zap.Int64("orderID", orderID), - zap.String("reason", reason), - zap.String("voidType", voidType)) - - return nil -} - -func (s *orderSvc) SplitBillRequest(ctx mycontext.Context, partnerID, orderID int64, splitType string, items []entity.SplitBillItem, amount float64) (*entity.Order, error) { - order, err := s.repo.FindByIDAndPartnerID(ctx, orderID, partnerID) - if err != nil { - logger.ContextLogger(ctx).Error("failed to find order for split bill", zap.Error(err)) - return nil, err - } - - if order.Status != "NEW" && order.Status != "PENDING" { - return nil, errors.New("only new or pending orders can be split") - } - - var splitOrder *entity.Order - - if splitType == "ITEM" { - splitOrder, err = s.splitByItems(ctx, order, items) - } else if splitType == "AMOUNT" { - splitOrder, err = s.splitByAmount(ctx, order, amount) - } - - if err != nil { - logger.ContextLogger(ctx).Error("failed to split bill", zap.Error(err)) - return nil, err - } - - logger.ContextLogger(ctx).Info("bill split successfully", - zap.Int64("orderID", orderID), - zap.String("splitType", splitType), - zap.Int64("splitOrderID", splitOrder.ID)) - - return splitOrder, nil -} - -func (s *orderSvc) splitByItems(ctx mycontext.Context, originalOrder *entity.Order, items []entity.SplitBillItem) (*entity.Order, error) { - var splitOrderItems []entity.OrderItem - orderItemMap := make(map[int64]*entity.OrderItem) - - for i := range originalOrder.OrderItems { - orderItemMap[originalOrder.OrderItems[i].ID] = &originalOrder.OrderItems[i] - } - - assignedItems := make(map[int64]bool) - - for _, item := range items { - orderItem, exists := orderItemMap[item.OrderItemID] - if !exists { - return nil, errors.New(fmt.Sprintf("order item %d not found", item.OrderItemID)) - } - - if item.Quantity > orderItem.Quantity { - return nil, errors.New(fmt.Sprintf("split quantity %d exceeds available quantity %d for item %d", - item.Quantity, orderItem.Quantity, item.OrderItemID)) - } - - if assignedItems[item.OrderItemID] { - return nil, errors.New(fmt.Sprintf("order item %d is already assigned to another split", item.OrderItemID)) - } - - assignedItems[item.OrderItemID] = true - - splitOrderItems = append(splitOrderItems, entity.OrderItem{ - ItemID: orderItem.ItemID, - ItemType: orderItem.ItemType, - Price: orderItem.Price, - ItemName: orderItem.ItemName, - Quantity: item.Quantity, - CreatedBy: originalOrder.CreatedBy, - Product: orderItem.Product, - Notes: orderItem.Notes, - }) - } - - splitAmount := 0.0 - for _, item := range splitOrderItems { - splitAmount += item.Price * float64(item.Quantity) - } - - splitTax := (splitAmount / originalOrder.Amount) * originalOrder.Tax - splitTotal := splitAmount + splitTax - - splitOrder := &entity.Order{ - PartnerID: originalOrder.PartnerID, - CustomerID: originalOrder.CustomerID, - CustomerName: originalOrder.CustomerName, - Status: "PAID", - Amount: splitAmount, - Tax: splitTax, - Total: splitTotal, - Source: originalOrder.Source, - CreatedBy: originalOrder.CreatedBy, - OrderItems: splitOrderItems, - OrderType: originalOrder.OrderType, - TableNumber: originalOrder.TableNumber, - CashierSessionID: originalOrder.CashierSessionID, - } - - createdOrder, err := s.repo.Create(ctx, splitOrder) - if err != nil { - logger.ContextLogger(ctx).Error("failed to create split order", zap.Error(err)) - return nil, err - } - - for _, item := range items { - orderItem := orderItemMap[item.OrderItemID] - newQuantity := orderItem.Quantity - item.Quantity - - if newQuantity == 0 { - err = s.repo.UpdateOrderItem(ctx, item.OrderItemID, 0) - } else { - err = s.repo.UpdateOrderItem(ctx, item.OrderItemID, newQuantity) - } - - if err != nil { - logger.ContextLogger(ctx).Error("failed to update original order item", zap.Error(err)) - return nil, err - } - } - - remainingAmount := originalOrder.Amount - splitAmount - remainingTax := (remainingAmount / originalOrder.Amount) * originalOrder.Tax - remainingTotal := remainingAmount + remainingTax - - err = s.repo.UpdateOrderTotals(ctx, originalOrder.ID, remainingAmount, remainingTax, remainingTotal) - if err != nil { - logger.ContextLogger(ctx).Error("failed to update original order totals", zap.Error(err)) - return nil, err - } - - return createdOrder, nil -} - -// splitByAmount splits the order by assigning specific amounts to each split -func (s *orderSvc) splitByAmount(ctx mycontext.Context, originalOrder *entity.Order, amount float64) (*entity.Order, error) { - // Validate that split amount is less than original order total - if amount >= originalOrder.Total { - return nil, errors.New(fmt.Sprintf("split amount %.2f must be less than order total %.2f", - amount, originalOrder.Total)) - } - - // For amount-based split, we create a new order with all items - var splitOrderItems []entity.OrderItem - - for _, item := range originalOrder.OrderItems { - splitOrderItems = append(splitOrderItems, entity.OrderItem{ - ItemID: item.ItemID, - ItemType: item.ItemType, - Price: item.Price, - ItemName: item.ItemName, - Quantity: item.Quantity, - CreatedBy: originalOrder.CreatedBy, - Product: item.Product, - Notes: item.Notes, - }) - } - - splitAmount := amount - splitTax := (splitAmount / originalOrder.Amount) * originalOrder.Tax - splitTotal := splitAmount + splitTax - - splitOrder := &entity.Order{ - PartnerID: originalOrder.PartnerID, - CustomerID: originalOrder.CustomerID, - CustomerName: originalOrder.CustomerName, - Status: "PAID", - Amount: splitAmount, - Tax: splitTax, - Total: splitTotal, - Source: originalOrder.Source, - CreatedBy: originalOrder.CreatedBy, - OrderItems: splitOrderItems, - OrderType: originalOrder.OrderType, - TableNumber: originalOrder.TableNumber, - CashierSessionID: originalOrder.CashierSessionID, - } - - createdOrder, err := s.repo.Create(ctx, splitOrder) - if err != nil { - logger.ContextLogger(ctx).Error("failed to create split order", zap.Error(err)) - return nil, err - } - - // Adjust original order amount - remainingAmount := originalOrder.Amount - splitAmount - remainingTax := (remainingAmount / originalOrder.Amount) * originalOrder.Tax - remainingTotal := remainingAmount + remainingTax - - // Update original order totals - err = s.repo.UpdateOrderTotals(ctx, originalOrder.ID, remainingAmount, remainingTax, remainingTotal) - if err != nil { - logger.ContextLogger(ctx).Error("failed to update original order totals", zap.Error(err)) - return nil, err - } - - return createdOrder, nil -} diff --git a/internal/services/v2/order/create_order_inquiry.go b/internal/services/v2/order/create_order_inquiry.go deleted file mode 100644 index 22b429c..0000000 --- a/internal/services/v2/order/create_order_inquiry.go +++ /dev/null @@ -1,248 +0,0 @@ -package order - -import ( - "enaklo-pos-be/internal/common/errors" - "enaklo-pos-be/internal/common/logger" - "enaklo-pos-be/internal/common/mycontext" - "enaklo-pos-be/internal/constants" - "enaklo-pos-be/internal/entity" - "go.uber.org/zap" - "math" -) - -func (s *orderSvc) CreateOrderInquiry(ctx mycontext.Context, - req *entity.OrderRequest) (*entity.OrderInquiryResponse, error) { - - cashierSession, err := s.cashierSvc.GetOpenSession(ctx, ctx.RequestedBy()) - if err != nil { - logger.ContextLogger(ctx).Error("no open session found for cashier", zap.Error(err)) - return nil, err - } - - productIDs, filteredItems, err := s.ValidateOrderItems(ctx, req.OrderItems) - if err != nil { - return nil, err - } - req.OrderItems = filteredItems - - productDetails, err := s.product.GetProductDetails(ctx, productIDs, req.PartnerID) - if err != nil { - logger.ContextLogger(ctx).Error("failed to get product details", zap.Error(err)) - return nil, err - } - - orderCalculation, err := s.CalculateOrderTotals(ctx, req.OrderItems, productDetails, req.Source, req.PartnerID) - if err != nil { - return nil, err - } - - customerID := int64(0) - - if req.CustomerID != nil && *req.CustomerID != 0 { - customer, err := s.customer.GetCustomer(ctx, *req.CustomerID) - if err != nil { - logger.ContextLogger(ctx).Error("customer is not found", zap.Error(err)) - return nil, err - } - customerID = customer.ID - } - - if err != nil { - logger.ContextLogger(ctx).Error("failed to resolve customer", zap.Error(err)) - return nil, err - } - - inquiry := entity.NewOrderInquiry( - req.PartnerID, - customerID, - orderCalculation.Subtotal, - orderCalculation.Tax, - orderCalculation.Total, - req.PaymentMethod, - req.Source, - req.CreatedBy, - req.CustomerName, - req.CustomerPhoneNumber, - req.CustomerEmail, - req.PaymentProvider, - req.TableNumber, - req.OrderType, - cashierSession.ID, - ) - - for _, item := range req.OrderItems { - product := productDetails.Products[item.ProductID] - inquiry.AddOrderItem(item, product) - } - - savedInquiry, err := s.repo.CreateInquiry(ctx, inquiry) - if err != nil { - logger.ContextLogger(ctx).Error("failed to create order inquiry", zap.Error(err)) - return nil, err - } - - token, err := s.crypt.GenerateJWTOrderInquiry(savedInquiry) - if err != nil { - logger.ContextLogger(ctx).Error("failed to generate token", zap.Error(err)) - return nil, err - } - - return &entity.OrderInquiryResponse{ - OrderInquiry: savedInquiry, - Token: token, - }, nil -} - -func (s *orderSvc) ValidateOrderItems(ctx mycontext.Context, items []entity.OrderItemRequest) ([]int64, []entity.OrderItemRequest, error) { - var productIDs []int64 - var filteredItems []entity.OrderItemRequest - - for _, item := range items { - if item.Quantity <= 0 { - continue - } - productIDs = append(productIDs, item.ProductID) - filteredItems = append(filteredItems, item) - } - - if len(productIDs) == 0 { - return nil, nil, errors.ErrorBadRequest - } - - return productIDs, filteredItems, nil -} - -func (s *orderSvc) CalculateOrderTotals( - ctx mycontext.Context, - items []entity.OrderItemRequest, - productDetails *entity.ProductDetails, - source string, - partnerID int64, -) (*entity.OrderCalculation, error) { - subtotal := 0.0 - - for _, item := range items { - product, ok := productDetails.Products[item.ProductID] - if !ok { - return nil, errors.NewError(errors.ErrorInvalidRequest.ErrorType(), "product not found") - } - subtotal += product.Price * float64(item.Quantity) - } - - setting, err := s.partnerSetting.GetSettings(ctx, partnerID) - - if err != nil { - return nil, errors.NewError(errors.ErrorInvalidRequest.ErrorType(), "failed to get partner settings") - } - - tax := 0.0 - if setting.TaxEnabled { - tax = (setting.TaxPercentage / 100) * subtotal - tax = math.Round(tax/100) * 100 - } - - return &entity.OrderCalculation{ - Subtotal: subtotal, - Tax: tax, - Total: subtotal + tax, - }, nil -} - -func (s *orderSvc) validateInquiry(ctx mycontext.Context, token string) (*entity.OrderInquiry, error) { - partnerID, inquiryID, err := s.crypt.ValidateJWTOrderInquiry(token) - if err != nil { - return nil, errors.NewError(errors.ErrorInvalidRequest.ErrorType(), "inquiry is not valid or expired") - } - - if partnerID != *ctx.GetPartnerID() { - return nil, errors.NewError(errors.ErrorInvalidRequest.ErrorType(), "invalid request") - } - - inquiry, err := s.repo.FindInquiryByID(ctx, inquiryID) - if err != nil { - logger.ContextLogger(ctx).Error("error when finding inquiry", zap.Error(err)) - return nil, err - } - - if inquiry.Status != constants.StatusPending { - return nil, errors.NewError(errors.ErrorInvalidRequest.ErrorType(), "inquiry is no longer pending") - } - - return inquiry, nil -} - -func (s *orderSvc) GetOrderPaymentAnalysis( - ctx mycontext.Context, - partnerID int64, - req entity.SearchRequest, -) (*entity.OrderPaymentAnalysis, error) { - paymentBreakdown, err := s.repo.GetOrderPaymentMethodBreakdown(ctx, partnerID, req) - if err != nil { - return nil, err - } - - var totalAmount float64 - var totalTransactions int64 - - for _, breakdown := range paymentBreakdown { - totalAmount += breakdown.TotalAmount - totalTransactions += breakdown.TotalTransactions - } - - return &entity.OrderPaymentAnalysis{ - TotalAmount: totalAmount, - TotalTransactions: totalTransactions, - PaymentMethodBreakdown: paymentBreakdown, - }, nil -} - -func (s *orderSvc) GetRevenueOverview( - ctx mycontext.Context, - partnerID int64, - year int, - granularity string, - status string, -) ([]entity.RevenueOverviewItem, error) { - req := entity.RevenueOverviewRequest{ - PartnerID: partnerID, - Year: year, - Granularity: granularity, - Status: status, - } - - return s.repo.GetRevenueOverview(ctx, req) -} - -func (s *orderSvc) GetSalesByCategory( - ctx mycontext.Context, - partnerID int64, - period string, - status string, -) ([]entity.SalesByCategoryItem, error) { - req := entity.SalesByCategoryRequest{ - PartnerID: partnerID, - Period: period, - Status: status, - } - - return s.repo.GetSalesByCategory(ctx, req) -} - -func (s *orderSvc) GetPopularProducts( - ctx mycontext.Context, - partnerID int64, - period string, - status string, - limit int, - sortBy string, -) ([]entity.PopularProductItem, error) { - req := entity.PopularProductsRequest{ - PartnerID: partnerID, - Period: period, - Status: status, - Limit: limit, - SortBy: sortBy, - } - - return s.repo.GetPopularProducts(ctx, req) -} diff --git a/internal/services/v2/order/execute_order.go b/internal/services/v2/order/execute_order.go deleted file mode 100644 index 84e395f..0000000 --- a/internal/services/v2/order/execute_order.go +++ /dev/null @@ -1,321 +0,0 @@ -package order - -import ( - "enaklo-pos-be/internal/common/logger" - "enaklo-pos-be/internal/common/mycontext" - "enaklo-pos-be/internal/constants" - "enaklo-pos-be/internal/entity" - "fmt" - "github.com/pkg/errors" - "go.uber.org/zap" - "time" -) - -func (s *orderSvc) ExecuteOrderInquiry(ctx mycontext.Context, - token string, paymentMethod, paymentProvider string, inprogressOrderID int64) (*entity.OrderResponse, error) { - inquiry, err := s.validateInquiry(ctx, token) - if err != nil { - return nil, err - } - - order := inquiry.ToOrder(paymentMethod, paymentProvider) - order.InProgressOrderID = inprogressOrderID - - savedOrder, err := s.repo.Create(ctx, order) - if err != nil { - logger.ContextLogger(ctx).Error("failed to create order", zap.Error(err)) - return nil, err - } - - err = s.processPostOrderActions(ctx, savedOrder, inquiry.ID, paymentMethod) - if err != nil { - logger.ContextLogger(ctx).Warn("some post-order actions failed", zap.Error(err)) - } - - return &entity.OrderResponse{ - Order: savedOrder, - }, nil -} - -func (s *orderSvc) RefundRequest(ctx mycontext.Context, partnerID, orderID int64, reason string) error { - order, err := s.repo.FindByIDAndPartnerID(ctx, orderID, partnerID) - if err != nil { - logger.ContextLogger(ctx).Error("failed to find order for refund", zap.Error(err)) - return err - } - - if order.Status != "PAID" { - return errors.New("only paid order can be refund") - } - - err = s.repo.UpdateOrder(ctx, order.ID, "REFUNDED", reason) - if err != nil { - logger.ContextLogger(ctx).Error("failed to update order status", zap.Error(err)) - return err - } - - refundTransaction, err := s.createRefundTransaction(ctx, order, reason) - if err != nil { - logger.ContextLogger(ctx).Error("failed to create refund transaction", zap.Error(err)) - return err - } - - if order.CustomerID != nil && *order.CustomerID > 0 { - err = s.reverseCustomerVouchers(ctx, *order.CustomerID, int64(order.Total), order.ID) - if err != nil { - logger.ContextLogger(ctx).Warn("failed to reverse customer vouchers", zap.Error(err)) - } - } - - logger.ContextLogger(ctx).Info("refund processed successfully", - zap.Int64("orderID", orderID), - zap.String("reason", reason), - zap.String("refundTransactionID", refundTransaction.ID)) - - return nil -} - -func (s *orderSvc) processPostOrderActions( - ctx mycontext.Context, - order *entity.Order, - inquiryID string, - paymentMethod string, -) error { - err := s.repo.UpdateInquiryStatus(ctx, inquiryID, constants.StatusExecuted) - if err != nil { - logger.ContextLogger(ctx).Error("error when updating inquiry status", zap.Error(err)) - } - - trx, err := s.createTransaction(ctx, order, paymentMethod) - if err != nil { - logger.ContextLogger(ctx).Error("error when creating transaction", zap.Error(err)) - } - - if order.CustomerID != nil && *order.CustomerID > 0 { - err = s.addCustomerVouchers(ctx, *order.CustomerID, int64(order.Total), trx.OrderID) - if err != nil { - logger.ContextLogger(ctx).Error("error when adding points", zap.Error(err)) - } - } - - return nil -} - -func (s *orderSvc) createTransaction(ctx mycontext.Context, order *entity.Order, paymentMethod string) (*entity.Transaction, error) { - transaction := &entity.Transaction{ - ID: constants.GenerateUUID(), - OrderID: order.ID, - Amount: order.Total, - PaymentMethod: paymentMethod, - Status: "SUCCESS", - CreatedAt: constants.TimeNow(), - PartnerID: order.PartnerID, - TransactionType: "TRANSACTION", - } - - _, err := s.transaction.Create(ctx, transaction) - - return transaction, err -} - -func (s *orderSvc) addCustomerVouchers(ctx mycontext.Context, customerID int64, total int64, reference int64) error { - undians, err := s.voucherUndianRepo.GetActiveUndianEvents(ctx) - if err != nil { - return err - } - - eligibleVoucher := []*entity.UndianVoucherDB{} - totalVouchersNeeded := 0 - - for _, v := range undians { - if total >= int64(v.MinimumPurchase) { - voucherCount := int(total / int64(v.MinimumPurchase)) - totalVouchersNeeded += voucherCount - } - } - - if totalVouchersNeeded == 0 { - return nil - } - - startSequence, err := s.voucherUndianRepo.GetNextVoucherSequenceBatch(ctx, totalVouchersNeeded) - if err != nil { - return err - } - - currentSequence := startSequence - - for _, v := range undians { - if total >= int64(v.MinimumPurchase) { - voucherCount := int(total / int64(v.MinimumPurchase)) - - for i := 0; i < voucherCount; i++ { - voucherCode := s.generateVoucherCode(v.ID, reference, currentSequence) - - voucher := &entity.UndianVoucherDB{ - UndianEventID: v.ID, - CustomerID: customerID, - VoucherCode: voucherCode, - VoucherNumber: &i, - IsWinner: false, - CreatedAt: time.Now(), - } - - eligibleVoucher = append(eligibleVoucher, voucher) - currentSequence++ - } - } - } - - return s.voucherUndianRepo.CreateUndianVouchers(ctx, eligibleVoucher) -} - -func (s *orderSvc) generateVoucherCode(eventID int64, reference int64, sequence int64) string { - eventPart := eventID % 100 // Last 2 digits of event ID - sequencePart := sequence % 100000 // Last 5 digits of sequence - orderPart := reference % 1000 // Last 3 digits of order ID - - return fmt.Sprintf("%02d%05d%03d", eventPart, sequencePart, orderPart) -} - -func (s *orderSvc) sendTransactionReceipt(ctx mycontext.Context, order *entity.Order, transaction *entity.Transaction, paymentMethod string) error { - newPoint := int(order.Total / 50000) - - if newPoint <= 0 { - return nil - } - - if order.CustomerID == nil || *order.CustomerID == 0 { - return nil - } - - customerPoint, err := s.customer.GetCustomerPoints(ctx, *order.CustomerID) - if err != nil { - return nil - } - - customer, err := s.customer.GetCustomer(ctx, *order.CustomerID) - if err != nil { - logger.ContextLogger(ctx).Error("error getting customer details", zap.Error(err)) - return err - } - - branchName := "Bakso 343 Rawamangun" - - var productIDs []int64 - productIDMap := make(map[int64]bool) - for _, item := range order.OrderItems { - if item.ItemID > 0 && !productIDMap[item.ItemID] { - productIDs = append(productIDs, item.ItemID) - productIDMap[item.ItemID] = true - } - } - - productMap := make(map[int64]*entity.Product) - if len(productIDs) > 0 { - products, err := s.product.GetProductsByIDs(ctx, productIDs, order.PartnerID) - if err != nil { - logger.ContextLogger(ctx).Error("error fetching products", zap.Error(err)) - } else { - for _, product := range products { - productMap[product.ID] = product - } - } - } - - var itemsData []map[string]string - for _, item := range order.OrderItems { - itemName := "Item" - - if product, exists := productMap[item.ItemID]; exists { - itemName = product.Name - } - - itemsData = append(itemsData, map[string]string{ - "ItemName": itemName, - "Quantity": fmt.Sprintf("%d", item.Quantity), - "Price": fmt.Sprintf("Rp %s", formatCurrency(item.Price)), - }) - } - - transactionDate := transaction.CreatedAt.Format("02 January 2006 15:04") - viewTransactionLink := "https://web.enaklo.co.id" - - emailData := map[string]interface{}{ - "UserName": customer.Name, - "PointsName": "Point", - "PointsBalance": newPoint, - "NewPoints": newPoint, - "TotalPoints": customerPoint.TotalPoints, - "RedeemLink": "web.enaklo.co.id", - "BranchName": branchName, - "TransactionNumber": order.ID, - "TransactionDate": transactionDate, - "PaymentMethod": formatPaymentMethod(paymentMethod), - "Items": itemsData, - "TotalPayment": fmt.Sprintf("Rp %s", formatCurrency(order.Total)), - "ViewTransactionLink": viewTransactionLink, - "ExpiryDate": order.CreatedAt.Format("02 January 2006"), - "UndianDate": "17 Mei 2025", - "WebURL": "https://web.enaklo.co.id/undian", - } - - return s.notification.SendEmailTransactional(ctx, entity.SendEmailNotificationParam{ - Sender: "noreply@enaklo.co.id", - Recipient: customer.Email, - Subject: "Hore, kamu dapat poin!", - TemplateName: "monthly_points", - TemplatePath: "/templates/monthly_points.html", - Data: emailData, - }) -} - -func formatCurrency(amount float64) string { - return fmt.Sprintf("%.2f", amount) -} - -func formatPaymentMethod(method string) string { - methodMap := map[string]string{ - "CASH": "Tunai", - "QRIS": "QRIS", - "CARD": "Kartu Kredit/Debit", - } - - if displayName, exists := methodMap[method]; exists { - return displayName - } - return method -} - -func (s *orderSvc) createRefundTransaction(ctx mycontext.Context, order *entity.Order, reason string) (*entity.Transaction, error) { - transaction := &entity.Transaction{ - OrderID: order.ID, - Amount: -order.Total, - PaymentMethod: order.PaymentType, - Status: "REFUND", - CreatedAt: constants.TimeNow(), - PartnerID: order.PartnerID, - TransactionType: "REFUND", - CreatedBy: ctx.RequestedBy(), - UpdatedBy: ctx.RequestedBy(), - } - - _, err := s.transaction.Create(ctx, transaction) - return transaction, err -} - -func (s *orderSvc) reverseCustomerVouchers(ctx mycontext.Context, customerID int64, total int64, orderID int64) error { - // Find vouchers associated with this order and reverse them - // This is a simplified implementation - in production you might want to track voucher-order relationships - logger.ContextLogger(ctx).Info("reversing customer vouchers", - zap.Int64("customerID", customerID), - zap.Int64("orderID", orderID)) - - // TODO: Implement voucher reversal logic - // This would involve: - // 1. Finding vouchers created for this order - // 2. Marking them as reversed/cancelled - // 3. Optionally adjusting customer points - - return nil -} diff --git a/internal/services/v2/order/order.go b/internal/services/v2/order/order.go deleted file mode 100644 index 8db5f27..0000000 --- a/internal/services/v2/order/order.go +++ /dev/null @@ -1,165 +0,0 @@ -package order - -import ( - "context" - "enaklo-pos-be/internal/common/mycontext" - "enaklo-pos-be/internal/entity" -) - -type Repository interface { - Create(ctx mycontext.Context, order *entity.Order) (*entity.Order, error) - FindByID(ctx mycontext.Context, id int64) (*entity.Order, error) - CreateInquiry(ctx mycontext.Context, inquiry *entity.OrderInquiry) (*entity.OrderInquiry, error) - FindInquiryByID(ctx mycontext.Context, id string) (*entity.OrderInquiry, error) - UpdateInquiryStatus(ctx mycontext.Context, id string, status string) error - UpdateOrder(ctx mycontext.Context, id int64, status string, description string) error - UpdateOrderItem(ctx mycontext.Context, orderItemID int64, quantity int) error - UpdateOrderTotals(ctx mycontext.Context, orderID int64, amount, tax, total float64) error - CreateOrderItem(ctx mycontext.Context, orderID int64, item *entity.OrderItem) error - GetOrderHistoryByPartnerID(ctx mycontext.Context, partnerID *int64, req entity.SearchRequest) ([]*entity.Order, int64, error) - GetOrderPaymentMethodBreakdown(ctx mycontext.Context, partnerID int64, req entity.SearchRequest) ([]entity.PaymentMethodBreakdown, error) - GetRevenueOverview(ctx mycontext.Context, req entity.RevenueOverviewRequest) ([]entity.RevenueOverviewItem, error) - GetSalesByCategory(ctx mycontext.Context, req entity.SalesByCategoryRequest) ([]entity.SalesByCategoryItem, error) - GetPopularProducts(ctx mycontext.Context, req entity.PopularProductsRequest) ([]entity.PopularProductItem, error) - GetOrderHistoryByUserID(ctx mycontext.Context, userID int64, req entity.SearchRequest) ([]*entity.Order, int64, error) - FindByIDAndPartnerID(ctx mycontext.Context, id int64, partnerID int64) (*entity.Order, error) - FindByIDAndCustomerID(ctx mycontext.Context, id int64, customerID int64) (*entity.Order, error) - DeleteOrderItem(ctx mycontext.Context, orderItemID int64) error -} - -type ProductService interface { - GetProductDetails(ctx mycontext.Context, productIDs []int64, partnerID int64) (*entity.ProductDetails, error) - GetProductsByIDs(ctx mycontext.Context, ids []int64, partnerID int64) ([]*entity.Product, error) -} - -type CustomerService interface { - ResolveCustomer(ctx mycontext.Context, req *entity.CustomerResolutionRequest) (int64, error) - AddPoints(ctx mycontext.Context, customerID int64, points int, reference string) error - GetCustomer(ctx mycontext.Context, id int64) (*entity.Customer, error) - GetCustomerPoints(ctx mycontext.Context, customerID int64) (*entity.CustomerPoints, error) -} - -type TransactionService interface { - Create(ctx mycontext.Context, transaction *entity.Transaction) (*entity.Transaction, error) -} - -type CryptService interface { - GenerateJWTOrderInquiry(inquiry *entity.OrderInquiry) (string, error) - ValidateJWTOrderInquiry(tokenString string) (int64, string, error) -} - -type NotificationService interface { - SendEmailTransactional(ctx context.Context, param entity.SendEmailNotificationParam) error -} - -type Service interface { - CreateOrderInquiry(ctx mycontext.Context, - req *entity.OrderRequest) (*entity.OrderInquiryResponse, error) - ExecuteOrderInquiry(ctx mycontext.Context, - token string, paymentMethod, paymentProvider string, inProgressOrderID int64) (*entity.OrderResponse, error) - RefundRequest(ctx mycontext.Context, partnerID, orderID int64, reason string) error - PartialRefundRequest(ctx mycontext.Context, partnerID, orderID int64, reason string, items []entity.PartialRefundItem) error - VoidOrderRequest(ctx mycontext.Context, partnerID, orderID int64, reason string, voidType string, items []entity.VoidItem) error - SplitBillRequest(ctx mycontext.Context, partnerID, orderID int64, splitType string, items []entity.SplitBillItem, amount float64) (*entity.Order, error) - GetOrderHistory(ctx mycontext.Context, request entity.SearchRequest) ([]*entity.Order, int64, error) - CalculateOrderTotals( - ctx mycontext.Context, - items []entity.OrderItemRequest, - productDetails *entity.ProductDetails, - source string, - partnerID int64, - ) (*entity.OrderCalculation, error) - ValidateOrderItems(ctx mycontext.Context, items []entity.OrderItemRequest) ([]int64, []entity.OrderItemRequest, error) - GetOrderPaymentAnalysis( - ctx mycontext.Context, - partnerID int64, - req entity.SearchRequest, - ) (*entity.OrderPaymentAnalysis, error) - GetRevenueOverview( - ctx mycontext.Context, - partnerID int64, - year int, - granularity string, - status string, - ) ([]entity.RevenueOverviewItem, error) - GetSalesByCategory( - ctx mycontext.Context, - partnerID int64, - period string, - status string, - ) ([]entity.SalesByCategoryItem, error) - GetPopularProducts( - ctx mycontext.Context, - partnerID int64, - period string, - status string, - limit int, - sortBy string, - ) ([]entity.PopularProductItem, error) - GetCustomerOrderHistory(ctx mycontext.Context, userID int64, request entity.SearchRequest) ([]*entity.Order, int64, error) - GetOrderByOrderAndCustomerID(ctx mycontext.Context, customerID int64, orderID int64) (*entity.Order, error) - GetOrderByID(ctx mycontext.Context, orderID int64) (*entity.Order, error) - GetOrderByIDAndPartnerID(ctx mycontext.Context, orderID int64, partnerID int64) (*entity.Order, error) -} - -type Config interface { - GetOrderFee(source string) float64 -} - -type PartnerSettings interface { - GetSettings(ctx mycontext.Context, partnerID int64) (*entity.PartnerSettings, error) -} - -type InProgressOrderRepository interface { - GetListByPartnerID(ctx mycontext.Context, partnerID int64, limit, offset int) ([]*entity.InProgressOrder, error) -} - -type VoucherUndianRepo interface { - GetActiveUndianEvents(ctx mycontext.Context) ([]*entity.UndianEventDB, error) - GetNextVoucherSequenceBatch(ctx mycontext.Context, count int) (int64, error) - CreateUndianVouchers(ctx mycontext.Context, vouchers []*entity.UndianVoucherDB) error -} - -type CashierSvc interface { - GetOpenSession(ctx mycontext.Context, cashierID int64) (*entity.CashierSession, error) -} - -type orderSvc struct { - repo Repository - product ProductService - customer CustomerService - transaction TransactionService - crypt CryptService - cfg Config - notification NotificationService - partnerSetting PartnerSettings - inprogressOrder InProgressOrderRepository - voucherUndianRepo VoucherUndianRepo - cashierSvc CashierSvc -} - -func New( - repo Repository, - product ProductService, - customer CustomerService, - transaction TransactionService, - crypt CryptService, - cfg Config, - notification NotificationService, - partnerSetting PartnerSettings, - voucherUndianRepo VoucherUndianRepo, - cashierSvc CashierSvc, -) Service { - return &orderSvc{ - repo: repo, - product: product, - customer: customer, - transaction: transaction, - crypt: crypt, - cfg: cfg, - notification: notification, - partnerSetting: partnerSetting, - voucherUndianRepo: voucherUndianRepo, - cashierSvc: cashierSvc, - } -} diff --git a/internal/services/v2/order/order_history.go b/internal/services/v2/order/order_history.go deleted file mode 100644 index 936bce7..0000000 --- a/internal/services/v2/order/order_history.go +++ /dev/null @@ -1,55 +0,0 @@ -package order - -import ( - "enaklo-pos-be/internal/common/logger" - "enaklo-pos-be/internal/common/mycontext" - "enaklo-pos-be/internal/entity" - - "github.com/pkg/errors" - "go.uber.org/zap" -) - -func (s *orderSvc) GetOrderHistory(ctx mycontext.Context, request entity.SearchRequest) ([]*entity.Order, int64, error) { - return s.repo.GetOrderHistoryByPartnerID(ctx, ctx.GetPartnerID(), request) -} - -func (s *orderSvc) GetCustomerOrderHistory(ctx mycontext.Context, userID int64, request entity.SearchRequest) ([]*entity.Order, int64, error) { - return s.repo.GetOrderHistoryByUserID(ctx, userID, request) -} - -func (s *orderSvc) GetOrderByOrderAndCustomerID(ctx mycontext.Context, customerID int64, orderID int64) (*entity.Order, error) { - orders, err := s.repo.FindByIDAndCustomerID(ctx, orderID, customerID) - if err != nil { - logger.ContextLogger(ctx).Error("failed to get in-progress orders by partner ID", - zap.Error(err), - zap.Int64("customerID", customerID)) - return nil, errors.Wrap(err, "failed to get order") - } - - return orders, nil -} - -func (s *orderSvc) GetOrderByID(ctx mycontext.Context, orderID int64) (*entity.Order, error) { - order, err := s.repo.FindByID(ctx, orderID) - if err != nil { - logger.ContextLogger(ctx).Error("failed to get order by ID", - zap.Error(err), - zap.Int64("orderID", orderID)) - return nil, errors.Wrap(err, "failed to get order") - } - - return order, nil -} - -func (s *orderSvc) GetOrderByIDAndPartnerID(ctx mycontext.Context, orderID int64, partnerID int64) (*entity.Order, error) { - order, err := s.repo.FindByIDAndPartnerID(ctx, orderID, partnerID) - if err != nil { - logger.ContextLogger(ctx).Error("failed to get order by ID and partner ID", - zap.Error(err), - zap.Int64("orderID", orderID), - zap.Int64("partnerID", partnerID)) - return nil, errors.Wrap(err, "failed to get order") - } - - return order, nil -} diff --git a/internal/services/v2/partner_settings/partner_setting.go b/internal/services/v2/partner_settings/partner_setting.go deleted file mode 100644 index ea88542..0000000 --- a/internal/services/v2/partner_settings/partner_setting.go +++ /dev/null @@ -1,149 +0,0 @@ -package partner_settings - -import ( - "enaklo-pos-be/internal/common/mycontext" - "enaklo-pos-be/internal/entity" - "enaklo-pos-be/internal/repository" - "encoding/json" - "github.com/pkg/errors" -) - -type PartnerSettingsService interface { - GetSettings(ctx mycontext.Context, partnerID int64) (*entity.PartnerSettings, error) - UpdateSettings(ctx mycontext.Context, settings *entity.PartnerSettings) error - GetPaymentMethods(ctx mycontext.Context, partnerID int64) ([]entity.PartnerPaymentMethod, error) - AddPaymentMethod(ctx mycontext.Context, method *entity.PartnerPaymentMethod) error - UpdatePaymentMethod(ctx mycontext.Context, method *entity.PartnerPaymentMethod) error - DeletePaymentMethod(ctx mycontext.Context, id int64, partnerID int64) error - ReorderPaymentMethods(ctx mycontext.Context, partnerID int64, methodIDs []int64) error - GetBusinessHours(ctx mycontext.Context, partnerID int64) (*entity.BusinessHoursSetting, error) - UpdateBusinessHours(ctx mycontext.Context, partnerID int64, hours *entity.BusinessHoursSetting) error -} - -type partnerSettingsService struct { - settingsRepo repository.PartnerSettingsRepository -} - -func NewPartnerSettingsService(settingsRepo repository.PartnerSettingsRepository) PartnerSettingsService { - return &partnerSettingsService{ - settingsRepo: settingsRepo, - } -} - -func (s *partnerSettingsService) GetSettings(ctx mycontext.Context, partnerID int64) (*entity.PartnerSettings, error) { - return s.settingsRepo.GetByPartnerID(ctx, partnerID) -} - -func (s *partnerSettingsService) UpdateSettings(ctx mycontext.Context, settings *entity.PartnerSettings) error { - if settings == nil { - return errors.New("settings cannot be nil") - } - - // Validate tax percentage - if settings.TaxEnabled && (settings.TaxPercentage < 0 || settings.TaxPercentage > 100) { - return errors.New("tax percentage must be between 0 and 100") - } - - return s.settingsRepo.Upsert(ctx, settings) -} - -func (s *partnerSettingsService) GetPaymentMethods(ctx mycontext.Context, partnerID int64) ([]entity.PartnerPaymentMethod, error) { - return s.settingsRepo.GetPaymentMethods(ctx, partnerID) -} - -func (s *partnerSettingsService) AddPaymentMethod(ctx mycontext.Context, method *entity.PartnerPaymentMethod) error { - if method == nil { - return errors.New("payment method cannot be nil") - } - - method.ID = 0 - - return s.settingsRepo.UpsertPaymentMethod(ctx, method) -} - -func (s *partnerSettingsService) UpdatePaymentMethod(ctx mycontext.Context, method *entity.PartnerPaymentMethod) error { - if method == nil { - return errors.New("payment method cannot be nil") - } - - if method.ID <= 0 { - return errors.New("invalid payment method ID") - } - - return s.settingsRepo.UpsertPaymentMethod(ctx, method) -} - -func (s *partnerSettingsService) DeletePaymentMethod(ctx mycontext.Context, id int64, partnerID int64) error { - if id <= 0 { - return errors.New("invalid payment method ID") - } - - return s.settingsRepo.DeletePaymentMethod(ctx, id, partnerID) -} - -func (s *partnerSettingsService) ReorderPaymentMethods(ctx mycontext.Context, partnerID int64, methodIDs []int64) error { - if len(methodIDs) == 0 { - return errors.New("method IDs cannot be empty") - } - - return s.settingsRepo.UpdatePaymentMethodOrder(ctx, partnerID, methodIDs) -} - -// GetBusinessHours retrieves parsed business hours for a partner -func (s *partnerSettingsService) GetBusinessHours(ctx mycontext.Context, partnerID int64) (*entity.BusinessHoursSetting, error) { - settings, err := s.settingsRepo.GetByPartnerID(ctx, partnerID) - if err != nil { - return nil, err - } - - // Create default hours if not set - if settings.BusinessHours == "" { - defaultHours := createDefaultBusinessHours() - return defaultHours, nil - } - - var hours entity.BusinessHoursSetting - if err := json.Unmarshal([]byte(settings.BusinessHours), &hours); err != nil { - return nil, errors.Wrap(err, "failed to parse business hours") - } - - return &hours, nil -} - -func (s *partnerSettingsService) UpdateBusinessHours(ctx mycontext.Context, partnerID int64, hours *entity.BusinessHoursSetting) error { - if hours == nil { - return errors.New("business hours cannot be nil") - } - - settings, err := s.settingsRepo.GetByPartnerID(ctx, partnerID) - if err != nil { - return err - } - - // Serialize hours to JSON - hoursJSON, err := json.Marshal(hours) - if err != nil { - return errors.Wrap(err, "failed to serialize business hours") - } - - settings.BusinessHours = string(hoursJSON) - - return s.settingsRepo.Upsert(ctx, settings) -} - -func createDefaultBusinessHours() *entity.BusinessHoursSetting { - defaultDay := entity.DayHours{ - Open: "08:00", - Close: "22:00", - } - - return &entity.BusinessHoursSetting{ - Monday: defaultDay, - Tuesday: defaultDay, - Wednesday: defaultDay, - Thursday: defaultDay, - Friday: defaultDay, - Saturday: defaultDay, - Sunday: defaultDay, - } -} diff --git a/internal/services/v2/product/get_product_by_id.go b/internal/services/v2/product/get_product_by_id.go deleted file mode 100644 index c2e77f9..0000000 --- a/internal/services/v2/product/get_product_by_id.go +++ /dev/null @@ -1,43 +0,0 @@ -package product - -import ( - "enaklo-pos-be/internal/common/logger" - "enaklo-pos-be/internal/common/mycontext" - "enaklo-pos-be/internal/entity" - "github.com/pkg/errors" - "go.uber.org/zap" -) - -func (s *productSvc) GetProductsByIDs(ctx mycontext.Context, ids []int64, partnerID int64) ([]*entity.Product, error) { - if len(ids) == 0 { - return []*entity.Product{}, nil - } - - products, err := s.repo.GetProductsByIDs(ctx, ids, partnerID) - if err != nil { - logger.ContextLogger(ctx).Error("failed to get products by IDs", - zap.Int64s("productIDs", ids), - zap.Int64("partnerID", partnerID), - zap.Error(err)) - return nil, errors.Wrap(err, "failed to get products by IDs") - } - - // Validate that we found all requested products - if len(products) != len(ids) { - logger.ContextLogger(ctx).Warn("some products not found", - zap.Int("requestedCount", len(ids)), - zap.Int("foundCount", len(products))) - } - - return products, nil -} - -func (s *productSvc) GetProductsByPartnerID(ctx mycontext.Context, search entity.ProductSearch) ([]*entity.Product, int64, error) { - products, total, err := s.repo.GetProductsByPartnerID(ctx, search) - - if err != nil { - return nil, 0, errors.Wrap(err, "failed to get products by partner ID") - } - - return products, total, nil -} diff --git a/internal/services/v2/product/get_product_details.go b/internal/services/v2/product/get_product_details.go deleted file mode 100644 index 961b9f6..0000000 --- a/internal/services/v2/product/get_product_details.go +++ /dev/null @@ -1,56 +0,0 @@ -package product - -import ( - "enaklo-pos-be/internal/common/logger" - "enaklo-pos-be/internal/common/mycontext" - "enaklo-pos-be/internal/entity" - "github.com/pkg/errors" - "go.uber.org/zap" -) - -func (s *productSvc) GetProductDetails(ctx mycontext.Context, productIDs []int64, partnerID int64) (*entity.ProductDetails, error) { - if len(productIDs) == 0 { - return &entity.ProductDetails{ - Products: make(map[int64]*entity.Product), - }, nil - } - - productDetails, err := s.repo.GetProductDetails(ctx, productIDs, partnerID) - if err != nil { - logger.ContextLogger(ctx).Error("failed to get product details", - zap.Int64s("productIDs", productIDs), - zap.Int64("partnerID", partnerID), - zap.Error(err)) - return nil, errors.Wrap(err, "failed to get product details") - } - - if len(productDetails.Products) != len(productIDs) { - missingIDs := findMissingProductIDs(productIDs, productDetails.Products) - logger.ContextLogger(ctx).Warn("some products not found", - zap.Int("requestedCount", len(productIDs)), - zap.Int("foundCount", len(productDetails.Products)), - zap.Int64s("missingIDs", missingIDs)) - - if len(productDetails.Products) == 0 { - return nil, errors.New("no products found") - } - } - - return productDetails, nil -} - -func findMissingProductIDs(requestedIDs []int64, foundProducts map[int64]*entity.Product) []int64 { - var missingIDs []int64 - - for _, id := range requestedIDs { - if _, exists := foundProducts[id]; !exists { - missingIDs = append(missingIDs, id) - } - } - - return missingIDs -} - -func (s *productSvc) IsProductAvailable(product *entity.Product) bool { - return product.Status == "ACTIVE" -} diff --git a/internal/services/v2/product/product.go b/internal/services/v2/product/product.go deleted file mode 100644 index 02a0d1b..0000000 --- a/internal/services/v2/product/product.go +++ /dev/null @@ -1,28 +0,0 @@ -package product - -import ( - "enaklo-pos-be/internal/common/mycontext" - "enaklo-pos-be/internal/entity" -) - -type Repository interface { - GetProductsByIDs(ctx mycontext.Context, ids []int64, partnerID int64) ([]*entity.Product, error) - GetProductDetails(ctx mycontext.Context, productIDs []int64, partnerID int64) (*entity.ProductDetails, error) - GetProductsByPartnerID(ctx mycontext.Context, req entity.ProductSearch) ([]*entity.Product, int64, error) -} - -type Service interface { - GetProductsByIDs(ctx mycontext.Context, ids []int64, partnerID int64) ([]*entity.Product, error) - GetProductDetails(ctx mycontext.Context, productIDs []int64, partnerID int64) (*entity.ProductDetails, error) - GetProductsByPartnerID(ctx mycontext.Context, search entity.ProductSearch) ([]*entity.Product, int64, error) -} - -type productSvc struct { - repo Repository -} - -func New(repo Repository) Service { - return &productSvc{ - repo: repo, - } -} diff --git a/internal/services/v2/undian/undian.go b/internal/services/v2/undian/undian.go deleted file mode 100644 index 2f1fd1e..0000000 --- a/internal/services/v2/undian/undian.go +++ /dev/null @@ -1,123 +0,0 @@ -package undian - -import ( - "enaklo-pos-be/internal/common/logger" - "enaklo-pos-be/internal/common/mycontext" - "enaklo-pos-be/internal/entity" - "github.com/pkg/errors" - "go.uber.org/zap" -) - -type Service interface { - GetUndianList(ctx mycontext.Context, customerID int64) (*entity.UndianListResponse, error) - GetActiveUndianEvents(ctx mycontext.Context) ([]*entity.UndianEventDB, error) -} - -type Repository interface { - GetUndianEventByID(ctx mycontext.Context, id int64) (*entity.UndianEventDB, error) - GetActiveUndianEvents(ctx mycontext.Context) ([]*entity.UndianEventDB, error) - GetActiveUndianEventsWithPrizes(ctx mycontext.Context, customerID int64) ([]*entity.UndianEventDB, error) - GetCustomerVouchersByEventIDs(ctx mycontext.Context, customerID int64, eventIDs []int64) ([]*entity.UndianVoucherDB, error) - CreateUndianVouchers(ctx mycontext.Context, vouchers []*entity.UndianVoucherDB) error - GetNextVoucherSequence(ctx mycontext.Context) (int64, error) - GetNextVoucherSequenceBatch(ctx mycontext.Context, count int) (int64, error) -} - -type undianSvc struct { - repo Repository -} - -func New(repo Repository) Service { - return &undianSvc{ - repo: repo, - } -} - -func (s *undianSvc) GetUndianList(ctx mycontext.Context, customerID int64) (*entity.UndianListResponse, error) { - events, err := s.repo.GetActiveUndianEventsWithPrizes(ctx, customerID) - if err != nil { - logger.ContextLogger(ctx).Error("failed to get active undian events with prizes", - zap.Int64("customerID", customerID), - zap.Error(err)) - return nil, errors.Wrap(err, "failed to get active undian events with prizes") - } - - if len(events) == 0 { - return &entity.UndianListResponse{ - Events: []*entity.UndianEventResponse{}, - }, nil - } - - // Build response - eventResponses := make([]*entity.UndianEventResponse, 0, len(events)) - for _, event := range events { - voucherResponses := make([]*entity.UndianVoucherResponse, 0, len(event.Vouchers)) - for _, voucher := range event.Vouchers { - voucherResponse := &entity.UndianVoucherResponse{ - ID: voucher.ID, - VoucherCode: voucher.VoucherCode, - VoucherNumber: voucher.VoucherNumber, - IsWinner: voucher.IsWinner, - PrizeRank: voucher.PrizeRank, - WonAt: voucher.WonAt, - CreatedAt: voucher.CreatedAt, - } - voucherResponses = append(voucherResponses, voucherResponse) - } - - // Convert prizes to response format - prizeResponses := make([]*entity.UndianPrizeResponse, 0, len(event.Prizes)) - for _, prize := range event.Prizes { - prizeResponse := &entity.UndianPrizeResponse{ - ID: prize.ID, - Rank: prize.Rank, - PrizeName: prize.PrizeName, - PrizeValue: prize.PrizeValue, - PrizeDescription: prize.PrizeDescription, - PrizeType: prize.PrizeType, - PrizeImageURL: prize.PrizeImageURL, - WinningVoucherID: prize.WinningVoucherID, - WinnerUserID: prize.WinnerUserID, - Amount: prize.Amount, - } - prizeResponses = append(prizeResponses, prizeResponse) - } - - eventResponse := &entity.UndianEventResponse{ - ID: event.ID, - Title: event.Title, - Description: event.Description, - ImageURL: event.ImageURL, - Status: event.Status, - StartDate: event.StartDate, - EndDate: event.EndDate, - DrawDate: event.DrawDate, - MinimumPurchase: event.MinimumPurchase, - DrawCompleted: event.DrawCompleted, - DrawCompletedAt: event.DrawCompletedAt, - TermsConditions: event.TermsAndConditions, - Prefix: event.Prefix, - CreatedAt: event.CreatedAt, - UpdatedAt: event.UpdatedAt, - VoucherCount: len(voucherResponses), // Fixed: use voucherResponses instead of eventVouchers - Vouchers: voucherResponses, // Fixed: use voucherResponses instead of eventVouchers - Prizes: prizeResponses, - } - - eventResponses = append(eventResponses, eventResponse) - } - - return &entity.UndianListResponse{ - Events: eventResponses, - }, nil -} - -func (s *undianSvc) GetActiveUndianEvents(ctx mycontext.Context) ([]*entity.UndianEventDB, error) { - events, err := s.repo.GetActiveUndianEvents(ctx) - if err != nil { - logger.ContextLogger(ctx).Error("failed to get active undian events", zap.Error(err)) - return nil, errors.Wrap(err, "failed to get active undian events") - } - - return events, nil -} diff --git a/internal/transformer/analytics_transformer.go b/internal/transformer/analytics_transformer.go new file mode 100644 index 0000000..920c9f4 --- /dev/null +++ b/internal/transformer/analytics_transformer.go @@ -0,0 +1,377 @@ +package transformer + +import ( + "apskel-pos-be/internal/contract" + "apskel-pos-be/internal/models" + "fmt" + "time" +) + +const ddmmyyyy = "02-01-2006" + +// PaymentMethodAnalyticsContractToModel converts contract request to model +func PaymentMethodAnalyticsContractToModel(req *contract.PaymentMethodAnalyticsRequest) *models.PaymentMethodAnalyticsRequest { + var dateFrom, dateTo time.Time + if req.DateFrom != "" { + df, err := time.Parse(ddmmyyyy, req.DateFrom) + if err == nil { + dateFrom = df + } + } + if req.DateTo != "" { + dt, err := time.Parse(ddmmyyyy, req.DateTo) + if err == nil { + dateTo = dt + } + } + + if req.DateFrom == req.DateTo { + dateTo.AddDate(0, 0, 1) + } + + return &models.PaymentMethodAnalyticsRequest{ + OrganizationID: req.OrganizationID, + OutletID: req.OutletID, + DateFrom: dateFrom, + DateTo: dateTo, + GroupBy: req.GroupBy, + } +} + +// PaymentMethodAnalyticsModelToContract converts model response to contract +func PaymentMethodAnalyticsModelToContract(resp *models.PaymentMethodAnalyticsResponse) *contract.PaymentMethodAnalyticsResponse { + if resp == nil { + return nil + } + + var data []contract.PaymentMethodAnalyticsData + for _, item := range resp.Data { + data = append(data, contract.PaymentMethodAnalyticsData{ + PaymentMethodID: item.PaymentMethodID, + PaymentMethodName: item.PaymentMethodName, + PaymentMethodType: item.PaymentMethodType, + TotalAmount: item.TotalAmount, + OrderCount: item.OrderCount, + PaymentCount: item.PaymentCount, + Percentage: item.Percentage, + }) + } + + return &contract.PaymentMethodAnalyticsResponse{ + OrganizationID: resp.OrganizationID, + OutletID: resp.OutletID, + DateFrom: resp.DateFrom, + DateTo: resp.DateTo, + GroupBy: resp.GroupBy, + Summary: contract.PaymentMethodSummary{ + TotalAmount: resp.Summary.TotalAmount, + TotalOrders: resp.Summary.TotalOrders, + TotalPayments: resp.Summary.TotalPayments, + AverageOrderValue: resp.Summary.AverageOrderValue, + }, + Data: data, + } +} + +func SalesAnalyticsContractToModel(req *contract.SalesAnalyticsRequest) *models.SalesAnalyticsRequest { + var dateFrom, dateTo time.Time + if req.DateFrom != "" { + df, err := time.Parse(ddmmyyyy, req.DateFrom) + if err == nil { + dateFrom = df + } + } + if req.DateTo != "" { + dt, err := time.Parse(ddmmyyyy, req.DateTo) + if err == nil { + dateTo = dt + } + } + + if req.DateFrom == req.DateTo { + dateTo.AddDate(0, 0, 1) + } + + return &models.SalesAnalyticsRequest{ + OrganizationID: req.OrganizationID, + OutletID: req.OutletID, + DateFrom: dateFrom, + DateTo: dateTo, + GroupBy: req.GroupBy, + } +} + +// SalesAnalyticsModelToContract converts model response to contract +func SalesAnalyticsModelToContract(resp *models.SalesAnalyticsResponse) *contract.SalesAnalyticsResponse { + if resp == nil { + return nil + } + + var data []contract.SalesAnalyticsData + for _, item := range resp.Data { + data = append(data, contract.SalesAnalyticsData{ + Date: item.Date, + Sales: item.Sales, + Orders: item.Orders, + Items: item.Items, + Tax: item.Tax, + Discount: item.Discount, + NetSales: item.NetSales, + }) + } + + return &contract.SalesAnalyticsResponse{ + OrganizationID: resp.OrganizationID, + OutletID: resp.OutletID, + DateFrom: resp.DateFrom, + DateTo: resp.DateTo, + GroupBy: resp.GroupBy, + Summary: contract.SalesSummary{ + TotalSales: resp.Summary.TotalSales, + TotalOrders: resp.Summary.TotalOrders, + TotalItems: resp.Summary.TotalItems, + AverageOrderValue: resp.Summary.AverageOrderValue, + TotalTax: resp.Summary.TotalTax, + TotalDiscount: resp.Summary.TotalDiscount, + NetSales: resp.Summary.NetSales, + }, + Data: data, + } +} + +// ProductAnalyticsContractToModel converts contract request to model +func ProductAnalyticsContractToModel(req *contract.ProductAnalyticsRequest) *models.ProductAnalyticsRequest { + var dateFrom, dateTo time.Time + if req.DateFrom != "" { + df, err := time.Parse(ddmmyyyy, req.DateFrom) + if err == nil { + dateFrom = df + } + } + if req.DateTo != "" { + dt, err := time.Parse(ddmmyyyy, req.DateTo) + if err == nil { + dateTo = dt + } + } + + if req.DateFrom == req.DateTo { + dateTo.AddDate(0, 0, 1) + } + + return &models.ProductAnalyticsRequest{ + OrganizationID: req.OrganizationID, + OutletID: req.OutletID, + DateFrom: dateFrom, + DateTo: dateTo, + Limit: req.Limit, + } +} + +// ProductAnalyticsModelToContract converts model response to contract +func ProductAnalyticsModelToContract(resp *models.ProductAnalyticsResponse) *contract.ProductAnalyticsResponse { + if resp == nil { + return nil + } + + var data []contract.ProductAnalyticsData + for _, item := range resp.Data { + data = append(data, contract.ProductAnalyticsData{ + ProductID: item.ProductID, + ProductName: item.ProductName, + CategoryID: item.CategoryID, + CategoryName: item.CategoryName, + QuantitySold: item.QuantitySold, + Revenue: item.Revenue, + AveragePrice: item.AveragePrice, + OrderCount: item.OrderCount, + }) + } + + return &contract.ProductAnalyticsResponse{ + OrganizationID: resp.OrganizationID, + OutletID: resp.OutletID, + DateFrom: resp.DateFrom, + DateTo: resp.DateTo, + Data: data, + } +} + +// DashboardAnalyticsContractToModel converts contract request to model +func DashboardAnalyticsContractToModel(req *contract.DashboardAnalyticsRequest) *models.DashboardAnalyticsRequest { + var dateFrom, dateTo time.Time + if req.DateFrom != "" { + df, err := time.Parse(ddmmyyyy, req.DateFrom) + if err == nil { + dateFrom = df + } + } + if req.DateTo != "" { + dt, err := time.Parse(ddmmyyyy, req.DateTo) + if err == nil { + dateTo = dt + } + } + return &models.DashboardAnalyticsRequest{ + OrganizationID: req.OrganizationID, + OutletID: req.OutletID, + DateFrom: dateFrom, + DateTo: dateTo, + } +} + +// DashboardAnalyticsModelToContract converts model response to contract +func DashboardAnalyticsModelToContract(resp *models.DashboardAnalyticsResponse) *contract.DashboardAnalyticsResponse { + if resp == nil { + return nil + } + + var topProducts []contract.ProductAnalyticsData + for _, item := range resp.TopProducts { + topProducts = append(topProducts, contract.ProductAnalyticsData{ + ProductID: item.ProductID, + ProductName: item.ProductName, + CategoryID: item.CategoryID, + CategoryName: item.CategoryName, + QuantitySold: item.QuantitySold, + Revenue: item.Revenue, + AveragePrice: item.AveragePrice, + OrderCount: item.OrderCount, + }) + } + + var paymentMethods []contract.PaymentMethodAnalyticsData + for _, item := range resp.PaymentMethods { + paymentMethods = append(paymentMethods, contract.PaymentMethodAnalyticsData{ + PaymentMethodID: item.PaymentMethodID, + PaymentMethodName: item.PaymentMethodName, + PaymentMethodType: item.PaymentMethodType, + TotalAmount: item.TotalAmount, + OrderCount: item.OrderCount, + PaymentCount: item.PaymentCount, + Percentage: item.Percentage, + }) + } + + var recentSales []contract.SalesAnalyticsData + for _, item := range resp.RecentSales { + recentSales = append(recentSales, contract.SalesAnalyticsData{ + Date: item.Date, + Sales: item.Sales, + Orders: item.Orders, + Items: item.Items, + Tax: item.Tax, + Discount: item.Discount, + NetSales: item.NetSales, + }) + } + + return &contract.DashboardAnalyticsResponse{ + OrganizationID: resp.OrganizationID, + OutletID: resp.OutletID, + DateFrom: resp.DateFrom, + DateTo: resp.DateTo, + Overview: contract.DashboardOverview{ + TotalSales: resp.Overview.TotalSales, + TotalOrders: resp.Overview.TotalOrders, + AverageOrderValue: resp.Overview.AverageOrderValue, + TotalCustomers: resp.Overview.TotalCustomers, + VoidedOrders: resp.Overview.VoidedOrders, + RefundedOrders: resp.Overview.RefundedOrders, + }, + TopProducts: topProducts, + PaymentMethods: paymentMethods, + RecentSales: recentSales, + } +} + +// ProfitLossAnalyticsContractToModel transforms contract request to model +func ProfitLossAnalyticsContractToModel(req *contract.ProfitLossAnalyticsRequest) (*models.ProfitLossAnalyticsRequest, error) { + if req == nil { + return nil, fmt.Errorf("request cannot be nil") + } + + dateFrom, err := time.Parse("02-01-2006", req.DateFrom) + if err != nil { + return nil, fmt.Errorf("invalid date_from format: %w", err) + } + + dateTo, err := time.Parse("02-01-2006", req.DateTo) + if err != nil { + return nil, fmt.Errorf("invalid date_to format: %w", err) + } + + return &models.ProfitLossAnalyticsRequest{ + OrganizationID: req.OrganizationID, + OutletID: req.OutletID, + DateFrom: dateFrom, + DateTo: dateTo, + GroupBy: req.GroupBy, + }, nil +} + +// ProfitLossAnalyticsModelToContract transforms model response to contract +func ProfitLossAnalyticsModelToContract(resp *models.ProfitLossAnalyticsResponse) *contract.ProfitLossAnalyticsResponse { + if resp == nil { + return nil + } + + // Transform profit/loss data + data := make([]contract.ProfitLossData, len(resp.Data)) + for i, item := range resp.Data { + data[i] = contract.ProfitLossData{ + Date: item.Date, + Revenue: item.Revenue, + Cost: item.Cost, + GrossProfit: item.GrossProfit, + GrossProfitMargin: item.GrossProfitMargin, + Tax: item.Tax, + Discount: item.Discount, + NetProfit: item.NetProfit, + NetProfitMargin: item.NetProfitMargin, + Orders: item.Orders, + } + } + + // Transform product profit data + productData := make([]contract.ProductProfitData, len(resp.ProductData)) + for i, item := range resp.ProductData { + productData[i] = contract.ProductProfitData{ + ProductID: item.ProductID, + ProductName: item.ProductName, + CategoryID: item.CategoryID, + CategoryName: item.CategoryName, + QuantitySold: item.QuantitySold, + Revenue: item.Revenue, + Cost: item.Cost, + GrossProfit: item.GrossProfit, + GrossProfitMargin: item.GrossProfitMargin, + AveragePrice: item.AveragePrice, + AverageCost: item.AverageCost, + ProfitPerUnit: item.ProfitPerUnit, + } + } + + return &contract.ProfitLossAnalyticsResponse{ + OrganizationID: resp.OrganizationID, + OutletID: resp.OutletID, + DateFrom: resp.DateFrom, + DateTo: resp.DateTo, + GroupBy: resp.GroupBy, + Summary: contract.ProfitLossSummary{ + TotalRevenue: resp.Summary.TotalRevenue, + TotalCost: resp.Summary.TotalCost, + GrossProfit: resp.Summary.GrossProfit, + GrossProfitMargin: resp.Summary.GrossProfitMargin, + TotalTax: resp.Summary.TotalTax, + TotalDiscount: resp.Summary.TotalDiscount, + NetProfit: resp.Summary.NetProfit, + NetProfitMargin: resp.Summary.NetProfitMargin, + TotalOrders: resp.Summary.TotalOrders, + AverageProfit: resp.Summary.AverageProfit, + ProfitabilityRatio: resp.Summary.ProfitabilityRatio, + }, + Data: data, + ProductData: productData, + } +} diff --git a/internal/transformer/category_transformer.go b/internal/transformer/category_transformer.go new file mode 100644 index 0000000..d16c7b1 --- /dev/null +++ b/internal/transformer/category_transformer.go @@ -0,0 +1,55 @@ +package transformer + +import ( + "apskel-pos-be/internal/appcontext" + "apskel-pos-be/internal/contract" + "apskel-pos-be/internal/models" +) + +func CreateCategoryRequestToModel(apctx *appcontext.ContextInfo, req *contract.CreateCategoryRequest) *models.CreateCategoryRequest { + return &models.CreateCategoryRequest{ + OrganizationID: apctx.OrganizationID, + Name: req.Name, + Description: req.Description, + ImageURL: nil, + SortOrder: 0, + } +} + +func UpdateCategoryRequestToModel(req *contract.UpdateCategoryRequest) *models.UpdateCategoryRequest { + return &models.UpdateCategoryRequest{ + Name: req.Name, + Description: req.Description, + ImageURL: nil, + SortOrder: nil, + IsActive: nil, + } +} + +func CategoryModelResponseToResponse(cat *models.CategoryResponse) *contract.CategoryResponse { + if cat == nil { + return nil + } + + return &contract.CategoryResponse{ + ID: cat.ID, + OrganizationID: cat.OrganizationID, + Name: cat.Name, + Description: cat.Description, + BusinessType: "restaurant", // Default business type + Metadata: map[string]interface{}{}, + CreatedAt: cat.CreatedAt, + UpdatedAt: cat.UpdatedAt, + } +} + +func CategoriesToResponses(categories []models.CategoryResponse) []contract.CategoryResponse { + responses := make([]contract.CategoryResponse, len(categories)) + for i, cat := range categories { + response := CategoryModelResponseToResponse(&cat) + if response != nil { + responses[i] = *response + } + } + return responses +} diff --git a/internal/transformer/common_transformer.go b/internal/transformer/common_transformer.go new file mode 100644 index 0000000..772fd95 --- /dev/null +++ b/internal/transformer/common_transformer.go @@ -0,0 +1,109 @@ +package transformer + +import ( + "apskel-pos-be/internal/contract" + "math" +) + +func PaginationToRequest(page, limit int) (int, int) { + if page < 1 { + page = 1 + } + if limit < 1 { + limit = 10 + } + if limit > 100 { + limit = 100 + } + return page, limit +} + +func CreatePaginationResponse(totalCount, page, limit int) contract.PaginationResponse { + totalPages := int(math.Ceil(float64(totalCount) / float64(limit))) + if totalPages < 1 { + totalPages = 1 + } + + return contract.PaginationResponse{ + TotalCount: totalCount, + Page: page, + Limit: limit, + TotalPages: totalPages, + } +} + +func CreateListUsersResponse(users []contract.UserResponse, totalCount, page, limit int) *contract.ListUsersResponse { + pagination := CreatePaginationResponse(totalCount, page, limit) + return &contract.ListUsersResponse{ + Users: users, + Pagination: pagination, + } +} + +func CreateListOrganizationsResponse(orgs []contract.OrganizationResponse, totalCount, page, limit int) *contract.ListOrganizationsResponse { + pagination := CreatePaginationResponse(totalCount, page, limit) + return &contract.ListOrganizationsResponse{ + Organizations: orgs, + TotalCount: pagination.TotalCount, + Page: pagination.Page, + Limit: pagination.Limit, + TotalPages: pagination.TotalPages, + } +} + +func CreateListOutletsResponse(outlets []contract.OutletResponse, totalCount, page, limit int) *contract.ListOutletsResponse { + pagination := CreatePaginationResponse(totalCount, page, limit) + return &contract.ListOutletsResponse{ + Outlets: outlets, + TotalCount: pagination.TotalCount, + Page: pagination.Page, + Limit: pagination.Limit, + TotalPages: pagination.TotalPages, + } +} + +func CreateListOrdersResponse(orders []contract.OrderResponse, totalCount, page, limit int) *contract.ListOrdersResponse { + pagination := CreatePaginationResponse(totalCount, page, limit) + return &contract.ListOrdersResponse{ + Orders: orders, + TotalCount: pagination.TotalCount, + Page: pagination.Page, + Limit: pagination.Limit, + TotalPages: pagination.TotalPages, + } +} + +func CreateListProductsResponse(products []contract.ProductResponse, totalCount, page, limit int) *contract.ListProductsResponse { + pagination := CreatePaginationResponse(totalCount, page, limit) + return &contract.ListProductsResponse{ + Products: products, + TotalCount: pagination.TotalCount, + Page: pagination.Page, + Limit: pagination.Limit, + TotalPages: pagination.TotalPages, + } +} + +func CreateErrorResponse(message string, code int) *contract.ErrorResponse { + return &contract.ErrorResponse{ + Error: "error", + Message: message, + Code: code, + } +} + +func CreateValidationErrorResponse(message string, details map[string]string) *contract.ValidationErrorResponse { + return &contract.ValidationErrorResponse{ + Error: "validation_error", + Message: message, + Details: details, + Code: 400, + } +} + +func CreateSuccessResponse(message string, data interface{}) *contract.SuccessResponse { + return &contract.SuccessResponse{ + Message: message, + Data: data, + } +} diff --git a/internal/transformer/customer_transformer.go b/internal/transformer/customer_transformer.go new file mode 100644 index 0000000..72812af --- /dev/null +++ b/internal/transformer/customer_transformer.go @@ -0,0 +1,82 @@ +package transformer + +import ( + "apskel-pos-be/internal/contract" + "apskel-pos-be/internal/models" +) + +// Contract to Model conversions +func CreateCustomerRequestToModel(req *contract.CreateCustomerRequest) *models.CreateCustomerRequest { + return &models.CreateCustomerRequest{ + Name: req.Name, + Email: req.Email, + Phone: req.Phone, + Address: req.Address, + } +} + +func UpdateCustomerRequestToModel(req *contract.UpdateCustomerRequest) *models.UpdateCustomerRequest { + return &models.UpdateCustomerRequest{ + Name: req.Name, + Email: req.Email, + Phone: req.Phone, + Address: req.Address, + IsActive: req.IsActive, + } +} + +func ListCustomersRequestToModel(req *contract.ListCustomersRequest) *models.ListCustomersQuery { + return &models.ListCustomersQuery{ + Page: req.Page, + Limit: req.Limit, + Search: req.Search, + IsActive: req.IsActive, + IsDefault: req.IsDefault, + SortBy: req.SortBy, + SortOrder: req.SortOrder, + } +} + +func SetDefaultCustomerRequestToModel(req *contract.SetDefaultCustomerRequest) *models.SetDefaultCustomerRequest { + return &models.SetDefaultCustomerRequest{ + CustomerID: req.CustomerID, + } +} + +// Model to Contract conversions +func CustomerModelToResponse(customer *models.CustomerResponse) *contract.CustomerResponse { + return &contract.CustomerResponse{ + ID: customer.ID, + OrganizationID: customer.OrganizationID, + Name: customer.Name, + Email: customer.Email, + Phone: customer.Phone, + Address: customer.Address, + IsDefault: customer.IsDefault, + IsActive: customer.IsActive, + Metadata: customer.Metadata, + CreatedAt: customer.CreatedAt, + UpdatedAt: customer.UpdatedAt, + } +} + +func CustomerResponsesToResponses(customers []models.CustomerResponse) []contract.CustomerResponse { + responses := make([]contract.CustomerResponse, len(customers)) + for i, customer := range customers { + response := CustomerModelToResponse(&customer) + if response != nil { + responses[i] = *response + } + } + return responses +} + +func PaginatedCustomerResponseToContract(paginatedResponse *models.PaginatedResponse[models.CustomerResponse]) *contract.PaginatedCustomerResponse { + return &contract.PaginatedCustomerResponse{ + Data: CustomerResponsesToResponses(paginatedResponse.Data), + TotalCount: int(paginatedResponse.Pagination.Total), + Page: paginatedResponse.Pagination.Page, + Limit: paginatedResponse.Pagination.Limit, + TotalPages: paginatedResponse.Pagination.TotalPages, + } +} diff --git a/internal/transformer/file_transformer.go b/internal/transformer/file_transformer.go new file mode 100644 index 0000000..48a60ef --- /dev/null +++ b/internal/transformer/file_transformer.go @@ -0,0 +1,142 @@ +package transformer + +import ( + "apskel-pos-be/internal/constants" + "apskel-pos-be/internal/contract" + "apskel-pos-be/internal/models" + "strconv" + "time" + + "github.com/google/uuid" +) + +// Contract to Model transformers +func UploadFileContractToModel(req *contract.UploadFileRequest) *models.UploadFileRequest { + if req == nil { + return nil + } + + modelReq := &models.UploadFileRequest{ + FileType: constants.FileType(req.FileType), + IsPublic: req.IsPublic, + Metadata: req.Metadata, + } + + return modelReq +} + +func UpdateFileContractToModel(req *contract.UpdateFileRequest) *models.UpdateFileRequest { + if req == nil { + return nil + } + + return &models.UpdateFileRequest{ + IsPublic: req.IsPublic, + Metadata: req.Metadata, + } +} + +func ListFilesQueryToModel(query *contract.ListFilesQuery) *models.ListFilesRequest { + if query == nil { + return nil + } + + req := &models.ListFilesRequest{ + Search: query.Search, + Page: query.Page, + Limit: query.Limit, + } + + // Parse UUID fields + if query.OrganizationID != "" { + if organizationID, err := uuid.Parse(query.OrganizationID); err == nil { + req.OrganizationID = &organizationID + } + } + + if query.UserID != "" { + if userID, err := uuid.Parse(query.UserID); err == nil { + req.UserID = &userID + } + } + + // Parse string fields + if query.FileType != "" { + fileType := constants.FileType(query.FileType) + req.FileType = &fileType + } + + if query.IsPublic != "" { + if isPublic, err := strconv.ParseBool(query.IsPublic); err == nil { + req.IsPublic = &isPublic + } + } + + // Parse date fields + if query.DateFrom != "" { + if dateFrom, err := time.Parse("2006-01-02", query.DateFrom); err == nil { + req.DateFrom = &dateFrom + } + } + + if query.DateTo != "" { + if dateTo, err := time.Parse("2006-01-02", query.DateTo); err == nil { + req.DateTo = &dateTo + } + } + + return req +} + +// Model to Contract transformers +func FileModelToContract(resp *models.FileResponse) *contract.FileResponse { + if resp == nil { + return nil + } + + return &contract.FileResponse{ + ID: resp.ID, + OrganizationID: resp.OrganizationID, + UserID: resp.UserID, + FileName: resp.FileName, + OriginalName: resp.OriginalName, + FileURL: resp.FileURL, + FileSize: resp.FileSize, + MimeType: resp.MimeType, + FileType: resp.FileType, + UploadPath: resp.UploadPath, + IsPublic: resp.IsPublic, + Metadata: resp.Metadata, + CreatedAt: resp.CreatedAt, + UpdatedAt: resp.UpdatedAt, + } +} + +func ListFilesModelToContract(resp *models.ListFilesResponse) *contract.ListFilesResponse { + if resp == nil { + return nil + } + + files := make([]*contract.FileResponse, len(resp.Files)) + for i, file := range resp.Files { + files[i] = FileModelToContract(file) + } + + return &contract.ListFilesResponse{ + Files: files, + TotalCount: resp.TotalCount, + Page: resp.Page, + Limit: resp.Limit, + TotalPages: resp.TotalPages, + } +} + +func UploadFileModelToContract(resp *models.UploadFileResponse) *contract.UploadFileResponse { + if resp == nil { + return nil + } + + return &contract.UploadFileResponse{ + File: *FileModelToContract(&resp.File), + } +} diff --git a/internal/transformer/inventory_transformer.go b/internal/transformer/inventory_transformer.go new file mode 100644 index 0000000..72e5156 --- /dev/null +++ b/internal/transformer/inventory_transformer.go @@ -0,0 +1,74 @@ +package transformer + +import ( + "apskel-pos-be/internal/contract" + "apskel-pos-be/internal/models" + "time" +) + +func CreateInventoryRequestToModel(req *contract.CreateInventoryRequest) *models.CreateInventoryRequest { + return &models.CreateInventoryRequest{ + OutletID: req.OutletID, + ProductID: req.ProductID, + Quantity: req.Quantity, + ReorderLevel: req.ReorderLevel, + } +} + +func UpdateInventoryRequestToModel(req *contract.UpdateInventoryRequest) *models.UpdateInventoryRequest { + return &models.UpdateInventoryRequest{ + Quantity: req.Quantity, + ReorderLevel: req.ReorderLevel, + } +} + +func AdjustInventoryRequestToModel(req *contract.AdjustInventoryRequest) *models.InventoryAdjustmentRequest { + return &models.InventoryAdjustmentRequest{ + Delta: req.Delta, + Reason: req.Reason, + } +} + +// Model to Contract conversions +func InventoryModelResponseToResponse(inv *models.InventoryResponse) *contract.InventoryResponse { + if inv == nil { + return nil + } + + return &contract.InventoryResponse{ + ID: inv.ID, + OutletID: inv.OutletID, + ProductID: inv.ProductID, + Quantity: inv.Quantity, + ReorderLevel: inv.ReorderLevel, + IsLowStock: inv.IsLowStock, + UpdatedAt: inv.UpdatedAt, + } +} + +func InventoryAdjustmentToResponse( + inventoryID, productID, outletID string, + previousQty, newQty, delta int, + reason string, +) *contract.InventoryAdjustmentResponse { + // This would need proper UUID parsing in a real implementation + return &contract.InventoryAdjustmentResponse{ + PreviousQty: previousQty, + NewQty: newQty, + Delta: delta, + Reason: reason, + AdjustedAt: time.Now(), + } +} + +// Slice conversions +func InventoryToResponses(inventory []models.InventoryResponse) []contract.InventoryResponse { + responses := make([]contract.InventoryResponse, len(inventory)) + for i, inv := range inventory { + response := InventoryModelResponseToResponse(&inv) + if response != nil { + responses[i] = *response + } + } + return responses +} diff --git a/internal/transformer/order_transformer.go b/internal/transformer/order_transformer.go new file mode 100644 index 0000000..2c9d7dd --- /dev/null +++ b/internal/transformer/order_transformer.go @@ -0,0 +1,353 @@ +package transformer + +import ( + "apskel-pos-be/internal/constants" + "apskel-pos-be/internal/contract" + "apskel-pos-be/internal/models" + "strconv" + "time" + + "github.com/google/uuid" +) + +func CreateOrderContractToModel(req *contract.CreateOrderRequest) *models.CreateOrderRequest { + items := make([]models.CreateOrderItemRequest, len(req.OrderItems)) + for i, item := range req.OrderItems { + items[i] = models.CreateOrderItemRequest{ + ProductID: item.ProductID, + ProductVariantID: item.ProductVariantID, + Quantity: item.Quantity, + UnitPrice: item.UnitPrice, // Now optional + Modifiers: item.Modifiers, + Notes: item.Notes, + Metadata: item.Metadata, + } + } + return &models.CreateOrderRequest{ + OutletID: req.OutletID, + UserID: req.UserID, + TableNumber: req.TableNumber, + OrderType: constants.OrderType(req.OrderType), + OrderItems: items, + Notes: req.Notes, + CustomerName: req.CustomerName, + } +} + +func UpdateOrderContractToModel(req *contract.UpdateOrderRequest) *models.UpdateOrderRequest { + var status *constants.OrderStatus + + if req.Status != nil { + stats := constants.OrderStatus(*req.Status) + status = &stats + } + return &models.UpdateOrderRequest{ + TableNumber: req.TableNumber, + Status: status, + DiscountAmount: req.DiscountAmount, + Notes: req.Notes, + Metadata: req.Metadata, + } +} + +func AddToOrderContractToModel(req *contract.AddToOrderRequest) *models.AddToOrderRequest { + items := make([]models.CreateOrderItemRequest, len(req.OrderItems)) + for i, item := range req.OrderItems { + items[i] = models.CreateOrderItemRequest{ + ProductID: item.ProductID, + ProductVariantID: item.ProductVariantID, + Quantity: item.Quantity, + UnitPrice: item.UnitPrice, // Now optional + Modifiers: item.Modifiers, + Notes: item.Notes, + Metadata: item.Metadata, + } + } + return &models.AddToOrderRequest{ + OrderItems: items, + Notes: req.Notes, + Metadata: req.Metadata, + } +} + +func VoidOrderContractToModel(req *contract.VoidOrderRequest) *models.VoidOrderRequest { + items := make([]models.VoidItemRequest, len(req.Items)) + for i, item := range req.Items { + items[i] = models.VoidItemRequest{ + OrderItemID: item.OrderItemID, + Quantity: item.Quantity, + } + } + return &models.VoidOrderRequest{ + OrderID: req.OrderID, + Reason: req.Reason, + Type: req.Type, + Items: items, + } +} + +func OrderModelToContract(resp *models.OrderResponse) *contract.OrderResponse { + if resp == nil { + return nil + } + items := make([]contract.OrderItemResponse, len(resp.OrderItems)) + for i, item := range resp.OrderItems { + items[i] = contract.OrderItemResponse{ + ID: item.ID, + OrderID: item.OrderID, + ProductID: item.ProductID, + ProductName: item.ProductName, + ProductVariantID: item.ProductVariantID, + ProductVariantName: item.ProductVariantName, + Quantity: item.Quantity, + UnitPrice: item.UnitPrice, + TotalPrice: item.TotalPrice, + Modifiers: item.Modifiers, + Notes: item.Notes, + Metadata: item.Metadata, + Status: string(item.Status), + CreatedAt: item.CreatedAt, + UpdatedAt: item.UpdatedAt, + } + } + return &contract.OrderResponse{ + ID: resp.ID, + OrderNumber: resp.OrderNumber, + OutletID: resp.OutletID, + UserID: resp.UserID, + TableNumber: resp.TableNumber, + OrderType: string(resp.OrderType), + Status: string(resp.Status), + Subtotal: resp.Subtotal, + TaxAmount: resp.TaxAmount, + DiscountAmount: resp.DiscountAmount, + TotalAmount: resp.TotalAmount, + Notes: resp.Notes, + Metadata: resp.Metadata, + CreatedAt: resp.CreatedAt, + UpdatedAt: resp.UpdatedAt, + OrderItems: items, + } +} + +func AddToOrderModelToContract(resp *models.AddToOrderResponse) *contract.AddToOrderResponse { + if resp == nil { + return nil + } + items := make([]contract.OrderItemResponse, len(resp.AddedItems)) + for i, item := range resp.AddedItems { + items[i] = contract.OrderItemResponse{ + ID: item.ID, + OrderID: item.OrderID, + ProductID: item.ProductID, + ProductName: item.ProductName, + ProductVariantID: item.ProductVariantID, + ProductVariantName: item.ProductVariantName, + Quantity: item.Quantity, + UnitPrice: item.UnitPrice, + TotalPrice: item.TotalPrice, + Modifiers: item.Modifiers, + Notes: item.Notes, + Metadata: item.Metadata, + Status: string(item.Status), + CreatedAt: item.CreatedAt, + UpdatedAt: item.UpdatedAt, + } + } + return &contract.AddToOrderResponse{ + OrderID: resp.OrderID, + OrderNumber: resp.OrderNumber, + AddedItems: items, + UpdatedOrder: *OrderModelToContract(&resp.UpdatedOrder), + } +} + +func SetOrderCustomerContractToModel(req *contract.SetOrderCustomerRequest) *models.SetOrderCustomerRequest { + return &models.SetOrderCustomerRequest{ + CustomerID: req.CustomerID, + } +} + +func SetOrderCustomerModelToContract(resp *models.SetOrderCustomerResponse) *contract.SetOrderCustomerResponse { + if resp == nil { + return nil + } + return &contract.SetOrderCustomerResponse{ + OrderID: resp.OrderID, + CustomerID: resp.CustomerID, + Message: resp.Message, + } +} + +func ListOrdersQueryToModel(query *contract.ListOrdersQuery) *models.ListOrdersRequest { + req := &models.ListOrdersRequest{ + Search: query.Search, + Page: query.Page, + Limit: query.Limit, + } + + // Parse UUID fields + if query.OrganizationID != "" { + if organizationID, err := uuid.Parse(query.OrganizationID); err == nil { + req.OrganizationID = &organizationID + } + } + + if query.OutletID != "" { + if outletID, err := uuid.Parse(query.OutletID); err == nil { + req.OutletID = &outletID + } + } + + if query.UserID != "" { + if userID, err := uuid.Parse(query.UserID); err == nil { + req.UserID = &userID + } + } + + if query.CustomerID != "" { + if customerID, err := uuid.Parse(query.CustomerID); err == nil { + req.CustomerID = &customerID + } + } + + // Parse enum fields + if query.OrderType != "" { + orderType := constants.OrderType(query.OrderType) + req.OrderType = &orderType + } + + if query.Status != "" { + status := constants.OrderStatus(query.Status) + req.Status = &status + } + + if query.PaymentStatus != "" { + paymentStatus := constants.PaymentStatus(query.PaymentStatus) + req.PaymentStatus = &paymentStatus + } + + // Parse boolean fields + if query.IsVoid != "" { + if isVoid, err := strconv.ParseBool(query.IsVoid); err == nil { + req.IsVoid = &isVoid + } + } + + if query.IsRefund != "" { + if isRefund, err := strconv.ParseBool(query.IsRefund); err == nil { + req.IsRefund = &isRefund + } + } + + // Parse date fields + if query.DateFrom != "" { + if dateFrom, err := time.Parse("2006-01-02", query.DateFrom); err == nil { + req.DateFrom = &dateFrom + } + } + + if query.DateTo != "" { + if dateTo, err := time.Parse("2006-01-02", query.DateTo); err == nil { + req.DateTo = &dateTo + } + } + + return req +} + +func ListOrdersModelToContract(resp *models.ListOrdersResponse) *contract.ListOrdersResponse { + if resp == nil { + return nil + } + + orders := make([]contract.OrderResponse, len(resp.Orders)) + for i, order := range resp.Orders { + orders[i] = *OrderModelToContract(&order) + } + + return &contract.ListOrdersResponse{ + Orders: orders, + TotalCount: resp.TotalCount, + Page: resp.Page, + Limit: resp.Limit, + TotalPages: resp.TotalPages, + } +} + +// Payment-related transformers +func CreatePaymentContractToModel(req *contract.CreatePaymentRequest) *models.CreatePaymentRequest { + paymentOrderItems := make([]models.CreatePaymentOrderItemRequest, len(req.PaymentOrderItems)) + for i, item := range req.PaymentOrderItems { + paymentOrderItems[i] = models.CreatePaymentOrderItemRequest{ + OrderItemID: item.OrderItemID, + Amount: item.Amount, + } + } + return &models.CreatePaymentRequest{ + OrderID: req.OrderID, + PaymentMethodID: req.PaymentMethodID, + Amount: req.Amount, + TransactionID: req.TransactionID, + SplitNumber: req.SplitNumber, + SplitTotal: req.SplitTotal, + SplitDescription: req.SplitDescription, + PaymentOrderItems: paymentOrderItems, + Metadata: req.Metadata, + } +} + +func PaymentModelToContract(resp *models.PaymentResponse) *contract.PaymentResponse { + if resp == nil { + return nil + } + + paymentOrderItems := make([]contract.PaymentOrderItemResponse, len(resp.PaymentOrderItems)) + for i, item := range resp.PaymentOrderItems { + paymentOrderItems[i] = contract.PaymentOrderItemResponse{ + ID: item.ID, + PaymentID: item.PaymentID, + OrderItemID: item.OrderItemID, + Amount: item.Amount, + CreatedAt: item.CreatedAt, + UpdatedAt: item.UpdatedAt, + } + } + + return &contract.PaymentResponse{ + ID: resp.ID, + OrderID: resp.OrderID, + PaymentMethodID: resp.PaymentMethodID, + Amount: resp.Amount, + Status: string(resp.Status), + TransactionID: resp.TransactionID, + SplitNumber: resp.SplitNumber, + SplitTotal: resp.SplitTotal, + SplitDescription: resp.SplitDescription, + RefundAmount: resp.RefundAmount, + RefundReason: resp.RefundReason, + RefundedAt: resp.RefundedAt, + RefundedBy: resp.RefundedBy, + Metadata: resp.Metadata, + CreatedAt: resp.CreatedAt, + UpdatedAt: resp.UpdatedAt, + PaymentOrderItems: paymentOrderItems, + } +} + +func RefundOrderContractToModel(req *contract.RefundOrderRequest) *models.RefundOrderRequest { + orderItems := make([]models.RefundOrderItemRequest, len(req.OrderItems)) + for i, item := range req.OrderItems { + orderItems[i] = models.RefundOrderItemRequest{ + OrderItemID: item.OrderItemID, + RefundQuantity: item.RefundQuantity, + RefundAmount: item.RefundAmount, + Reason: item.Reason, + } + } + return &models.RefundOrderRequest{ + Reason: req.Reason, + RefundAmount: req.RefundAmount, + OrderItems: orderItems, + } +} diff --git a/internal/transformer/organization_transformer.go b/internal/transformer/organization_transformer.go new file mode 100644 index 0000000..1e88964 --- /dev/null +++ b/internal/transformer/organization_transformer.go @@ -0,0 +1,91 @@ +package transformer + +import ( + "apskel-pos-be/internal/constants" + "apskel-pos-be/internal/contract" + "apskel-pos-be/internal/models" +) + +// Contract to Model conversions +func CreateOrganizationRequestToModel(req *contract.CreateOrganizationRequest) *models.CreateOrganizationRequest { + return &models.CreateOrganizationRequest{ + OrganizationName: req.OrganizationName, + OrganizationEmail: req.OrganizationEmail, + OrganizationPhoneNumber: req.OrganizationPhoneNumber, + PlanType: constants.PlanType(req.PlanType), + AdminName: req.AdminName, + AdminEmail: req.AdminEmail, + AdminPassword: req.AdminPassword, + OutletName: req.OutletName, + OutletAddress: req.OutletAddress, + OutletTimezone: req.OutletTimezone, + OutletCurrency: req.OutletCurrency, + } +} + +func UpdateOrganizationRequestToModel(req *contract.UpdateOrganizationRequest) *models.UpdateOrganizationRequest { + modelReq := &models.UpdateOrganizationRequest{ + Name: req.Name, + Email: req.Email, + PhoneNumber: req.PhoneNumber, + } + + if req.PlanType != nil { + planType := constants.PlanType(*req.PlanType) + modelReq.PlanType = &planType + } + + return modelReq +} + +// Model to Contract conversions +func OrganizationModelResponseToResponse(org *models.OrganizationResponse) *contract.OrganizationResponse { + return &contract.OrganizationResponse{ + ID: org.ID, + Name: org.Name, + Email: org.Email, + PhoneNumber: org.PhoneNumber, + PlanType: string(org.PlanType), + CreatedAt: org.CreatedAt, + UpdatedAt: org.UpdatedAt, + } +} + +func CreateOrganizationResponseToContract(resp *models.CreateOrganizationResponse) *contract.CreateOrganizationResponse { + return &contract.CreateOrganizationResponse{ + Organization: *OrganizationModelResponseToResponse(resp.Organization), + AdminUser: *UserModelResponseToResponse(resp.AdminUser), + DefaultOutlet: *OutletModelResponseToContract(resp.DefaultOutlet), + } +} + +func OutletModelResponseToContract(outlet *models.OutletResponse) *contract.OutletResponse { + address := "" + if outlet.Address != nil { + address = *outlet.Address + } + + return &contract.OutletResponse{ + ID: outlet.ID, + OrganizationID: outlet.OrganizationID, + Name: outlet.Name, + Address: address, + Currency: outlet.Currency, + TaxRate: outlet.TaxRate, + IsActive: outlet.IsActive, + CreatedAt: outlet.CreatedAt, + UpdatedAt: outlet.UpdatedAt, + } +} + +// Slice conversions +func OrganizationsToResponses(organizations []models.OrganizationResponse) []contract.OrganizationResponse { + responses := make([]contract.OrganizationResponse, len(organizations)) + for i, org := range organizations { + response := OrganizationModelResponseToResponse(&org) + if response != nil { + responses[i] = *response + } + } + return responses +} diff --git a/internal/transformer/outlet_transformer.go b/internal/transformer/outlet_transformer.go new file mode 100644 index 0000000..9af7b67 --- /dev/null +++ b/internal/transformer/outlet_transformer.go @@ -0,0 +1,45 @@ +package transformer + +import ( + "apskel-pos-be/internal/constants" + "apskel-pos-be/internal/contract" + "apskel-pos-be/internal/models" +) + +func CreateOutletRequestToModel(req *contract.CreateOutletRequest) *models.CreateOutletRequest { + return &models.CreateOutletRequest{ + OrganizationID: req.OrganizationID, + Name: req.Name, + Address: req.Address, + PhoneNumber: req.PhoneNumber, + BusinessType: constants.BusinessTypeRestaurant, + Currency: constants.Currency(req.Currency), + TaxRate: req.TaxRate, + } +} + +func UpdateOutletRequestToModel(req *contract.UpdateOutletRequest) *models.UpdateOutletRequest { + return &models.UpdateOutletRequest{ + Name: req.Name, + Address: req.Address, + PhoneNumber: req.PhoneNumber, + TaxRate: req.TaxRate, + IsActive: req.IsActive, + OrganizationID: req.OrganizationID, + } +} + +func OutletModelResponseToResponse(model *models.OutletResponse) contract.OutletResponse { + return contract.OutletResponse{ + ID: model.ID, + OrganizationID: model.OrganizationID, + Name: model.Name, + Address: *model.Address, + BusinessType: string(constants.BusinessTypeRestaurant), // Default business type + Currency: model.Currency, + TaxRate: model.TaxRate, + IsActive: model.IsActive, + CreatedAt: model.CreatedAt, + UpdatedAt: model.UpdatedAt, + } +} diff --git a/internal/transformer/product_transformer.go b/internal/transformer/product_transformer.go new file mode 100644 index 0000000..c3d202d --- /dev/null +++ b/internal/transformer/product_transformer.go @@ -0,0 +1,187 @@ +package transformer + +import ( + "apskel-pos-be/internal/appcontext" + "apskel-pos-be/internal/constants" + "apskel-pos-be/internal/contract" + "apskel-pos-be/internal/models" +) + +func CreateProductRequestToModel(apctx *appcontext.ContextInfo, req *contract.CreateProductRequest) *models.CreateProductRequest { + cost := float64(0) + if req.Cost != nil { + cost = *req.Cost + } + + businessType := constants.BusinessTypeRestaurant + if req.BusinessType != nil { + businessType = constants.BusinessType(*req.BusinessType) + } + + var variants []models.CreateProductVariantRequest + if req.Variants != nil { + variants = make([]models.CreateProductVariantRequest, len(req.Variants)) + for i, variant := range req.Variants { + variants[i] = models.CreateProductVariantRequest{ + ProductID: variant.ProductID, + Name: variant.Name, + PriceModifier: variant.PriceModifier, + Cost: variant.Cost, + Metadata: variant.Metadata, + } + } + } + + metadata := req.Metadata + if metadata == nil { + metadata = make(map[string]interface{}) + } + if req.Image != nil { + metadata["image"] = *req.Image + } + + return &models.CreateProductRequest{ + OrganizationID: apctx.OrganizationID, + CategoryID: req.CategoryID, + SKU: req.SKU, + Name: req.Name, + Description: req.Description, + Price: req.Price, + Cost: cost, + BusinessType: businessType, + Metadata: metadata, + Variants: variants, + } +} + +func UpdateProductRequestToModel(req *contract.UpdateProductRequest) *models.UpdateProductRequest { + metadata := req.Metadata + if metadata == nil { + metadata = make(map[string]interface{}) + } + if req.Image != nil { + metadata["image"] = *req.Image + } + return &models.UpdateProductRequest{ + CategoryID: req.CategoryID, + SKU: req.SKU, + Name: req.Name, + Description: req.Description, + Price: req.Price, + Cost: req.Cost, + Metadata: metadata, + IsActive: req.IsActive, + } +} + +// Model to Contract conversions +func ProductModelResponseToResponse(prod *models.ProductResponse) *contract.ProductResponse { + if prod == nil { + return nil + } + + // Convert variants to contract responses + var variantResponses []contract.ProductVariantResponse + if prod.Variants != nil { + variantResponses = make([]contract.ProductVariantResponse, len(prod.Variants)) + for i, variant := range prod.Variants { + variantResponses[i] = contract.ProductVariantResponse{ + ID: variant.ID, + ProductID: variant.ProductID, + Name: variant.Name, + PriceModifier: variant.PriceModifier, + Cost: variant.Cost, + Metadata: variant.Metadata, + CreatedAt: variant.CreatedAt, + UpdatedAt: variant.UpdatedAt, + } + } + } + + response := &contract.ProductResponse{ + ID: prod.ID, + OrganizationID: prod.OrganizationID, + CategoryID: prod.CategoryID, + SKU: prod.SKU, + Name: prod.Name, + Description: prod.Description, + Price: prod.Price, + Cost: prod.Cost, + BusinessType: string(prod.BusinessType), + Metadata: prod.Metadata, + IsActive: prod.IsActive, + CreatedAt: prod.CreatedAt, + UpdatedAt: prod.UpdatedAt, + Variants: variantResponses, + } + + return response +} + +// Slice conversions +func ProductsToResponses(products []models.ProductResponse) []contract.ProductResponse { + responses := make([]contract.ProductResponse, len(products)) + for i, prod := range products { + response := ProductModelResponseToResponse(&prod) + if response != nil { + responses[i] = *response + } + } + return responses +} + +// Product Variant Transformers +func CreateProductVariantRequestToModel(req *contract.CreateProductVariantRequest) *models.CreateProductVariantRequest { + if req == nil { + return nil + } + + return &models.CreateProductVariantRequest{ + ProductID: req.ProductID, + Name: req.Name, + PriceModifier: req.PriceModifier, + Cost: req.Cost, + Metadata: req.Metadata, + } +} + +func UpdateProductVariantRequestToModel(req *contract.UpdateProductVariantRequest) *models.UpdateProductVariantRequest { + if req == nil { + return nil + } + + return &models.UpdateProductVariantRequest{ + Name: req.Name, + PriceModifier: req.PriceModifier, + Cost: req.Cost, + Metadata: req.Metadata, + } +} + +func ProductVariantModelResponseToResponse(variant *models.ProductVariantResponse) *contract.ProductVariantResponse { + if variant == nil { + return nil + } + + return &contract.ProductVariantResponse{ + ID: variant.ID, + ProductID: variant.ProductID, + Name: variant.Name, + PriceModifier: variant.PriceModifier, + Cost: variant.Cost, + Metadata: variant.Metadata, + CreatedAt: variant.CreatedAt, + UpdatedAt: variant.UpdatedAt, + } +} + +func ProductVariantsToResponses(variants []models.ProductVariantResponse) []contract.ProductVariantResponse { + responses := make([]contract.ProductVariantResponse, len(variants)) + for i, variant := range variants { + response := ProductVariantModelResponseToResponse(&variant) + if response != nil { + responses[i] = *response + } + } + return responses +} diff --git a/internal/transformer/transformer.go b/internal/transformer/transformer.go new file mode 100644 index 0000000..dd742fc --- /dev/null +++ b/internal/transformer/transformer.go @@ -0,0 +1,38 @@ +package transformer + +import ( + "github.com/gin-gonic/gin" +) + +type Transformer interface { + Success(c *gin.Context, statusCode int, message string, data interface{}) + Error(c *gin.Context, statusCode int, message string, err error) +} + +type ResponseTransformer struct{} + +func NewTransformer() Transformer { + return &ResponseTransformer{} +} + +func (t *ResponseTransformer) Success(c *gin.Context, statusCode int, message string, data interface{}) { + response := gin.H{ + "success": true, + "message": message, + "data": data, + } + c.JSON(statusCode, response) +} + +func (t *ResponseTransformer) Error(c *gin.Context, statusCode int, message string, err error) { + response := gin.H{ + "success": false, + "message": message, + } + + if err != nil { + response["error"] = err.Error() + } + + c.JSON(statusCode, response) +} diff --git a/internal/transformer/user_transformer.go b/internal/transformer/user_transformer.go new file mode 100644 index 0000000..e8d1f72 --- /dev/null +++ b/internal/transformer/user_transformer.go @@ -0,0 +1,105 @@ +package transformer + +import ( + "apskel-pos-be/internal/constants" + "apskel-pos-be/internal/contract" + "apskel-pos-be/internal/models" +) + +func CreateUserRequestToModel(req *contract.CreateUserRequest) *models.CreateUserRequest { + return &models.CreateUserRequest{ + OrganizationID: req.OrganizationID, + OutletID: req.OutletID, + Name: req.Name, + Email: req.Email, + Password: req.Password, + Role: constants.UserRole(req.Role), + Permissions: req.Permissions, + } +} + +func UpdateUserRequestToModel(req *contract.UpdateUserRequest) *models.UpdateUserRequest { + modelReq := &models.UpdateUserRequest{ + OutletID: req.OutletID, + IsActive: req.IsActive, + } + + if req.Name != nil { + modelReq.Name = req.Name + } + + if req.Email != nil { + modelReq.Email = req.Email + } + + if req.Role != nil { + role := constants.UserRole(*req.Role) + modelReq.Role = &role + } + + if req.Permissions != nil { + modelReq.Permissions = req.Permissions + } + + return modelReq +} + +func ChangePasswordRequestToModel(req *contract.ChangePasswordRequest) *models.ChangePasswordRequest { + return &models.ChangePasswordRequest{ + CurrentPassword: req.CurrentPassword, + NewPassword: req.NewPassword, + } +} + +// Model to Contract conversions +func UserModelToResponse(user *models.User) *contract.UserResponse { + return &contract.UserResponse{ + ID: user.ID, + OrganizationID: user.OrganizationID, + OutletID: user.OutletID, + Name: user.Name, + Email: user.Email, + Role: string(user.Role), + Permissions: user.Permissions, + IsActive: user.IsActive, + CreatedAt: user.CreatedAt, + UpdatedAt: user.UpdatedAt, + } +} + +func UserModelResponseToResponse(userResponse *models.UserResponse) *contract.UserResponse { + return &contract.UserResponse{ + ID: userResponse.ID, + OrganizationID: userResponse.OrganizationID, + OutletID: userResponse.OutletID, + Name: userResponse.Name, + Email: userResponse.Email, + Role: string(userResponse.Role), + Permissions: userResponse.Permissions, + IsActive: userResponse.IsActive, + CreatedAt: userResponse.CreatedAt, + UpdatedAt: userResponse.UpdatedAt, + } +} + +func UsersToResponses(users []models.User) []contract.UserResponse { + responses := make([]contract.UserResponse, len(users)) + for i, user := range users { + response := UserModelToResponse(&user) + if response != nil { + responses[i] = *response + } + } + return responses +} + +func UserResponsesToResponses(userResponses []models.UserResponse) []contract.UserResponse { + responses := make([]contract.UserResponse, len(userResponses)) + for i, userResponse := range userResponses { + response := UserModelResponseToResponse(&userResponse) + if response != nil { + responses[i] = *response + } + } + return responses +} diff --git a/internal/util/http_util.go b/internal/util/http_util.go new file mode 100644 index 0000000..11db7db --- /dev/null +++ b/internal/util/http_util.go @@ -0,0 +1,49 @@ +package util + +import ( + "apskel-pos-be/internal/constants" + "apskel-pos-be/internal/contract" + "apskel-pos-be/internal/logger" + "encoding/json" + "net/http" + "net/url" +) + +func HandleResponse(w http.ResponseWriter, r *http.Request, response *contract.Response, methodName string) { + var statusCode int + if response.GetSuccess() { + statusCode = http.StatusOK + } else { + responseError := response.GetErrors()[0] + statusCode = MapErrorCodeToHttpStatus(responseError.GetCode()) + } + WriteResponse(w, r, *response, statusCode, methodName) +} + +func WriteResponse(w http.ResponseWriter, r *http.Request, resp contract.Response, statusCode int, methodName string) { + w.WriteHeader(statusCode) + response, err := json.Marshal(resp) + if err != nil { + logger.FromContext(r.Context()).Error(methodName, "unable to marshal json response", err) + } + _, err = w.Write(response) + if err != nil { + logger.FromContext(r.Context()).Error(methodName, "unable to write to response", err) + } +} + +func MapErrorCodeToHttpStatus(code string) int { + statusCode := constants.HttpErrorMap[code] + if statusCode == 0 { + return http.StatusInternalServerError + } + return statusCode +} + +func ExtractEndpointFromURL(requestURL string) string { + parsedURL, err := url.Parse(requestURL) + if err != nil { + return "/" + } + return parsedURL.Path +} diff --git a/internal/utils/.DS_Store b/internal/utils/.DS_Store deleted file mode 100644 index 8f31184593ab442d4c28b6a64c95361fa26d0f1b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKyG{c^3>-s>AexjZ_ZRqsRTRE}AAk@<6LC_2{;GTzpT_v1i0G0Ai3W`&dv<-E zTkRC*GXPtCZts8vfH~a}2M<&8=k61`s*Dloe8(P7_`rbu_I*-)KH=OOyy69K^dJ1^ z-DVhe=Sj*+0VyB_q<|EV0>4(kdoOK%o2V!Sq<|DSD&XITMtAImV`6+d7-9q<&X^A4 zI%WxC^8~RMj)~0BEUCn#T8$W%bmm*t^};bR>986;tWLI?P%KX8`&*R5dZMBfkOHR) zT<3P-{r{eRVg5fQX(t7wz`s(!X6uLbidU-MI=P(p+D3n*d(9W!jq9K=L^~!%JLbmQ e@iL0CuKAkJd*PTEbmoIj)X#wHB9j7tt-v?3?-j2A diff --git a/internal/utils/arrays.go b/internal/utils/arrays.go deleted file mode 100644 index dfa3db8..0000000 --- a/internal/utils/arrays.go +++ /dev/null @@ -1,20 +0,0 @@ -package utils - -import ( - "fmt" - "strings" -) - -func ArrayToString(a []int64, delim string) string { - return strings.Trim(strings.Replace(fmt.Sprint(a), " ", delim, -1), "[]") -} - -func Contains(slice []string, item string) bool { - set := make(map[string]struct{}, len(slice)) - for _, s := range slice { - set[s] = struct{}{} - } - - _, ok := set[item] - return ok -} \ No newline at end of file diff --git a/internal/utils/bank_code.go b/internal/utils/bank_code.go deleted file mode 100644 index b36a0e2..0000000 --- a/internal/utils/bank_code.go +++ /dev/null @@ -1,16 +0,0 @@ -package utils - -func BankName(bankCode string) string { - bankCodeMap := map[string]string{ - "001": "Bank Central Asia (BCA)", - "002": "Bank Rakyat Indonesia (BRI)", - "008": "Bank Mandiri", - "009": "Bank Negara Indonesia (BNI)", - "014": "Bank Tabungan Negara (BTN)", - } - - if bankName, exists := bankCodeMap[bankCode]; exists { - return bankName - } - return "Bank code not recognized" -} diff --git a/internal/utils/currency.go b/internal/utils/currency.go deleted file mode 100644 index c37c034..0000000 --- a/internal/utils/currency.go +++ /dev/null @@ -1,43 +0,0 @@ -package utils - -import ( - "fmt" - "math/rand" - "strconv" - "strings" - "time" -) - -func FormatCurrency(value float64) string { - currencyStr := strconv.FormatFloat(value, 'f', 2, 64) - split := strings.Split(currencyStr, ".") - n := len(split[0]) - if n <= 3 { - return fmt.Sprintf("Rp %s,%s", split[0], split[1]) - } - var result []string - for i := 0; i < n; i += 3 { - end := n - i - start := end - 3 - if start < 0 { - start = 0 - } - result = append([]string{split[0][start:end]}, result...) - } - currencyStr = strings.Join(result, ".") - return fmt.Sprintf("Rp %s,%s", currencyStr, split[1]) -} - -const charset = "abcdefghijklmnopqrstuvwxyz" + - "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" - -var seededRand *rand.Rand = rand.New( - rand.NewSource(time.Now().UnixNano())) - -func GenerateRandomString(length int) string { - b := make([]byte, length) - for i := range b { - b[i] = charset[seededRand.Intn(len(charset))] - } - return string(b) -} diff --git a/internal/utils/currency_test.go b/internal/utils/currency_test.go deleted file mode 100644 index 26bbff6..0000000 --- a/internal/utils/currency_test.go +++ /dev/null @@ -1,36 +0,0 @@ -package utils - -import "testing" - -func TestFormatCurrency(t *testing.T) { - type args struct { - value float64 - } - tests := []struct { - name string - args args - want string - }{ - { - name: "hundred", - args: args{ - value: 626000, - }, - want: "Rp 626.000,00", - }, - { - name: "million", - args: args{ - value: 62603300, - }, - want: "Rp 62.603.300,00", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := FormatCurrency(tt.args.value); got != tt.want { - t.Errorf("FormatCurrency() = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/internal/utils/format_validator_error.go b/internal/utils/format_validator_error.go deleted file mode 100644 index 564fddc..0000000 --- a/internal/utils/format_validator_error.go +++ /dev/null @@ -1,38 +0,0 @@ -package utils - -import ( - "fmt" - "github.com/go-playground/validator/v10" - "reflect" -) - -func formatValidationError(err error) string { - if _, ok := err.(*validator.InvalidValidationError); ok { - return err.Error() - } - - var errorMessage string - for _, err := range err.(validator.ValidationErrors) { - switch err.Tag() { - case "required": - errorMessage += fmt.Sprintf("The field '%s' is required.", err.Field()) - case "min": - if err.Kind() == reflect.Slice { - errorMessage += fmt.Sprintf("The field '%s' must contain at least %s items.", err.Field(), err.Param()) - } else { - errorMessage += fmt.Sprintf("The field '%s' must be at least %s.", err.Field(), err.Param()) - } - case "oneof": - errorMessage += fmt.Sprintf("The field '%s' must be one of [%s].", err.Field(), err.Param()) - case "email": - errorMessage += fmt.Sprintf("The field '%s' must be a valid email address.", err.Field()) - case "len": - errorMessage += fmt.Sprintf("The field '%s' must be exactly %s characters long.", err.Field(), err.Param()) - default: - errorMessage += fmt.Sprintf("The field '%s' is invalid.", err.Field()) - } - errorMessage += " " - } - - return errorMessage -} diff --git a/internal/utils/generator/string-generator.go b/internal/utils/generator/string-generator.go deleted file mode 100644 index ae26339..0000000 --- a/internal/utils/generator/string-generator.go +++ /dev/null @@ -1,42 +0,0 @@ -package generator - -import ( - "fmt" - uuid2 "github.com/gofrs/uuid" - "math/rand" - "strconv" - "time" - - "github.com/google/uuid" -) - -func MedicalRecordNumberRand() string { - return RandStringRunes(10) -} - -func RandStringRunes(n int) string { - rand.Seed(time.Now().UnixNano()) // seed the random number generator - - const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" - b := make([]byte, n) - for i := range b { - b[i] = letterBytes[rand.Intn(len(letterBytes))] - } - - return string(b) -} - -// format -> -