From c8a2d775e3c510e37063fb1f4c450663e512d2b7 Mon Sep 17 00:00:00 2001 From: Manish Tiwari <69393822+manish-9245@users.noreply.github.com> Date: Sat, 24 Oct 2026 00:03:10 +0530 Subject: [PATCH 1/3] feat(infra): AWS shop module, encrypted versioned bucket and log group (not applied) --- .gitignore | 4 +++ deploy/aws/terraform/main.tf | 20 +++++++++++ deploy/aws/terraform/modules/shop/main.tf | 44 +++++++++++++++++++++++ deploy/aws/terraform/outputs.tf | 9 +++++ deploy/aws/terraform/variables.tf | 14 ++++++++ 5 files changed, 91 insertions(+) create mode 100644 deploy/aws/terraform/main.tf create mode 100644 deploy/aws/terraform/modules/shop/main.tf create mode 100644 deploy/aws/terraform/outputs.tf create mode 100644 deploy/aws/terraform/variables.tf diff --git a/.gitignore b/.gitignore index cc034ee..2647e2e 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,7 @@ bin/ *.db-shm .agents/plans/ coverage.out + +# Terraform local state and providers +**/.terraform/ +*.tfstate* diff --git a/deploy/aws/terraform/main.tf b/deploy/aws/terraform/main.tf new file mode 100644 index 0000000..2bbdcd0 --- /dev/null +++ b/deploy/aws/terraform/main.tf @@ -0,0 +1,20 @@ +# Per-shop managed hosting in ap-south-1. Plain Terraform, no apply from this repository. +terraform { + required_version = ">= 1.6" + required_providers { + aws = { source = "hashicorp/aws", version = "~> 5.0" } + } +} + +provider "aws" { + region = "ap-south-1" + default_tags { + tags = { project = "kaasu", managed_by = "terraform" } + } +} + +module "shop" { + source = "./modules/shop" + shop_id = var.shop_id + kms_alias = var.kms_alias +} diff --git a/deploy/aws/terraform/modules/shop/main.tf b/deploy/aws/terraform/modules/shop/main.tf new file mode 100644 index 0000000..fb9445e --- /dev/null +++ b/deploy/aws/terraform/modules/shop/main.tf @@ -0,0 +1,44 @@ +variable "shop_id" { type = string } +variable "kms_alias" { type = string } + +data "aws_kms_alias" "shops" { + name = var.kms_alias +} + +# The shop's source of truth: encrypted with the customer-managed key, versioned, no public access. +resource "aws_s3_bucket" "data" { + bucket = "kaasu-${var.shop_id}-data" +} + +resource "aws_s3_bucket_versioning" "data" { + bucket = aws_s3_bucket.data.id + versioning_configuration { + status = "Enabled" + } +} + +resource "aws_s3_bucket_server_side_encryption_configuration" "data" { + bucket = aws_s3_bucket.data.id + rule { + apply_server_side_encryption_by_default { + sse_algorithm = "aws:kms" + kms_master_key_id = data.aws_kms_alias.shops.target_key_arn + } + } +} + +resource "aws_s3_bucket_public_access_block" "data" { + bucket = aws_s3_bucket.data.id + block_public_acls = true + block_public_policy = true + ignore_public_acls = true + restrict_public_buckets = true +} + +resource "aws_cloudwatch_log_group" "task" { + name = "/kaasu/${var.shop_id}" + retention_in_days = 90 +} + +output "bucket" { value = aws_s3_bucket.data.bucket } +output "log_group" { value = aws_cloudwatch_log_group.task.name } diff --git a/deploy/aws/terraform/outputs.tf b/deploy/aws/terraform/outputs.tf new file mode 100644 index 0000000..e49f7b7 --- /dev/null +++ b/deploy/aws/terraform/outputs.tf @@ -0,0 +1,9 @@ +output "bucket" { + value = module.shop.bucket + description = "Source of truth for the shop's encrypted data; restored on task start." +} + +output "log_group" { + value = module.shop.log_group + description = "CloudWatch log group for the shop's task." +} diff --git a/deploy/aws/terraform/variables.tf b/deploy/aws/terraform/variables.tf new file mode 100644 index 0000000..5480bae --- /dev/null +++ b/deploy/aws/terraform/variables.tf @@ -0,0 +1,14 @@ +variable "shop_id" { + type = string + description = "Lower-case id, 3 to 32 characters of a-z, 0-9 and hyphen." + validation { + condition = can(regex("^[a-z0-9-]{3,32}$", var.shop_id)) + error_message = "shop_id must be 3 to 32 lower-case letters, digits or hyphens." + } +} + +variable "kms_alias" { + type = string + description = "Alias of the customer-managed KMS key that encrypts the shop's bucket." + default = "alias/kaasu-shops" +} From b4f3663afa452c4628f5cb890035fbc5639624eb Mon Sep 17 00:00:00 2001 From: Manish Tiwari <69393822+manish-9245@users.noreply.github.com> Date: Sat, 24 Oct 2026 00:02:39 +0530 Subject: [PATCH 2/3] feat(integrations): WooCommerce gateway with integer-paise amounts and signed webhooks --- .github/workflows/woocommerce.yml | 18 ++++ .../kaasu-for-woocommerce/PUBLISHING.md | 8 ++ .../includes/class-kaasu-amount.php | 27 ++++++ .../includes/class-kaasu-gateway.php | 90 +++++++++++++++++++ .../includes/class-kaasu-signature.php | 28 ++++++ .../kaasu-for-woocommerce.php | 27 ++++++ .../kaasu-for-woocommerce/readme.txt | 9 ++ .../kaasu-for-woocommerce/uninstall.php | 5 ++ integrations/woocommerce/tests/run.php | 37 ++++++++ 9 files changed, 249 insertions(+) create mode 100644 .github/workflows/woocommerce.yml create mode 100644 integrations/woocommerce/kaasu-for-woocommerce/PUBLISHING.md create mode 100644 integrations/woocommerce/kaasu-for-woocommerce/includes/class-kaasu-amount.php create mode 100644 integrations/woocommerce/kaasu-for-woocommerce/includes/class-kaasu-gateway.php create mode 100644 integrations/woocommerce/kaasu-for-woocommerce/includes/class-kaasu-signature.php create mode 100644 integrations/woocommerce/kaasu-for-woocommerce/kaasu-for-woocommerce.php create mode 100644 integrations/woocommerce/kaasu-for-woocommerce/readme.txt create mode 100644 integrations/woocommerce/kaasu-for-woocommerce/uninstall.php create mode 100644 integrations/woocommerce/tests/run.php diff --git a/.github/workflows/woocommerce.yml b/.github/workflows/woocommerce.yml new file mode 100644 index 0000000..78a064d --- /dev/null +++ b/.github/workflows/woocommerce.yml @@ -0,0 +1,18 @@ +name: woocommerce +on: + pull_request: + paths: ['integrations/woocommerce/**'] + push: + branches: [main] + paths: ['integrations/woocommerce/**'] +permissions: + contents: read +jobs: + php: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # v2 # v2 + with: { php-version: '8.2' } + - run: find integrations/woocommerce -name '*.php' -print0 | xargs -0 -n1 php -l + - run: php integrations/woocommerce/tests/run.php diff --git a/integrations/woocommerce/kaasu-for-woocommerce/PUBLISHING.md b/integrations/woocommerce/kaasu-for-woocommerce/PUBLISHING.md new file mode 100644 index 0000000..6900c12 --- /dev/null +++ b/integrations/woocommerce/kaasu-for-woocommerce/PUBLISHING.md @@ -0,0 +1,8 @@ +# Publishing checklist (human steps) + +Not published. Before submitting to the WordPress plugin directory: + +- [ ] Test on a current WordPress and WooCommerce with HPOS on and off. +- [ ] Replace this checklist's placeholder screenshots in the readme. +- [ ] Confirm the license header matches the repository (AGPL-3.0-or-later). +- [ ] Submit through plugins.trac.wordpress.org; the directory review is a human decision. diff --git a/integrations/woocommerce/kaasu-for-woocommerce/includes/class-kaasu-amount.php b/integrations/woocommerce/kaasu-for-woocommerce/includes/class-kaasu-amount.php new file mode 100644 index 0000000..f2b3777 --- /dev/null +++ b/integrations/woocommerce/kaasu-for-woocommerce/includes/class-kaasu-amount.php @@ -0,0 +1,27 @@ +id = 'kaasu'; + $this->method_title = 'Kaasu'; + $this->method_description = 'UPI payments confirmed by your self-hosted Kaasu server.'; + $this->has_fields = false; + $this->supports = ['products']; + $this->init_form_fields(); + $this->init_settings(); + $this->title = $this->get_option('title', 'UPI (Kaasu)'); + $this->enabled = $this->get_option('enabled', 'no'); + add_action('woocommerce_update_options_payment_gateways_' . $this->id, [$this, 'process_admin_options']); + add_action('woocommerce_api_kaasu', [$this, 'handle_webhook']); + } + + public function init_form_fields(): void { + $this->form_fields = [ + 'enabled' => ['title' => 'Enable', 'type' => 'checkbox', 'default' => 'no'], + 'title' => ['title' => 'Title', 'type' => 'text', 'default' => 'UPI (Kaasu)'], + 'kaasu_url' => ['title' => 'Kaasu URL', 'type' => 'text', 'description' => 'https://pay.example.com'], + 'api_key' => ['title' => 'API key', 'type' => 'password'], + 'webhook_secret' => ['title' => 'Webhook secret', 'type' => 'password'], + ]; + } + + public function process_payment($order_id): array { + $order = wc_get_order($order_id); + $amount = Kaasu_Amount::to_paise((string) $order->get_total()); + $response = wp_remote_post(rtrim($this->get_option('kaasu_url'), '/') . '/v1/orders', [ + 'timeout' => 15, + 'headers' => [ + 'Authorization' => 'Bearer ' . $this->get_option('api_key'), + 'Content-Type' => 'application/json', + // Stable per order, so a retry never creates a second payment. + 'Idempotency-Key' => 'wc-' . $order_id . '-' . $order->get_order_key(), + ], + 'body' => wp_json_encode(['amount_paise' => $amount, 'reference' => (string) $order->get_order_number()]), + ]); + if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 201) { + wc_add_notice('Payment could not be started. Please try again.', 'error'); + return ['result' => 'failure']; + } + $data = json_decode(wp_remote_retrieve_body($response), true); + $order->update_meta_data('_kaasu_order_id', $data['id'] ?? ''); + $order->save(); + return [ + 'result' => 'success', + 'redirect' => rtrim($this->get_option('kaasu_url'), '/') . '/pay/' . rawurlencode((string) ($data['id'] ?? '')), + ]; + } + + public function handle_webhook(): void { + $body = file_get_contents('php://input'); + $sig = $_SERVER['HTTP_X_KAASU_SIGNATURE'] ?? ''; + if (!Kaasu_Signature::verify((string) $this->get_option('webhook_secret'), $sig, $body, time())) { + status_header(401); + exit; + } + $event = json_decode($body, true); + $event_id = (string) ($event['event_id'] ?? ''); + if ($event_id === '' || get_transient('kaasu_evt_' . $event_id)) { + status_header(200); + exit; + } + set_transient('kaasu_evt_' . $event_id, 1, DAY_IN_SECONDS); + if (($event['type'] ?? '') === 'order.confirmed') { + $kaasu_id = (string) ($event['data']['order']['id'] ?? ''); + $order = $this->find_order($kaasu_id); + if ($order && !$order->is_paid()) { + $order->payment_complete($kaasu_id); + } + } + status_header(200); + exit; + } + + private function find_order(string $kaasu_id): ?WC_Order { + $found = wc_get_orders(['meta_key' => '_kaasu_order_id', 'meta_value' => $kaasu_id, 'limit' => 1]); + return $found[0] ?? null; + } +} diff --git a/integrations/woocommerce/kaasu-for-woocommerce/includes/class-kaasu-signature.php b/integrations/woocommerce/kaasu-for-woocommerce/includes/class-kaasu-signature.php new file mode 100644 index 0000000..6f3a3ac --- /dev/null +++ b/integrations/woocommerce/kaasu-for-woocommerce/includes/class-kaasu-signature.php @@ -0,0 +1,28 @@ +,v1=. + */ +final class Kaasu_Signature { + public static function verify(string $secret, string $header, string $body, int $now, int $tolerance = 300): bool { + $parts = []; + foreach (explode(',', $header) as $p) { + $kv = explode('=', $p, 2); + if (count($kv) === 2) { + $parts[$kv[0]] = $kv[1]; + } + } + if (!isset($parts['t'], $parts['v1']) || !ctype_digit($parts['t'])) { + return false; + } + $t = (int) $parts['t']; + if (abs($now - $t) > $tolerance) { + return false; + } + $want = hash_hmac('sha256', $parts['t'] . '.' . $body, $secret); + return hash_equals($want, $parts['v1']); + } +} diff --git a/integrations/woocommerce/kaasu-for-woocommerce/kaasu-for-woocommerce.php b/integrations/woocommerce/kaasu-for-woocommerce/kaasu-for-woocommerce.php new file mode 100644 index 0000000..c709fbf --- /dev/null +++ b/integrations/woocommerce/kaasu-for-woocommerce/kaasu-for-woocommerce.php @@ -0,0 +1,27 @@ + Settings > Payments > Kaasu. +2. In Kaasu, point the webhook at https://your-store.example/?wc-api=kaasu and use the same secret. diff --git a/integrations/woocommerce/kaasu-for-woocommerce/uninstall.php b/integrations/woocommerce/kaasu-for-woocommerce/uninstall.php new file mode 100644 index 0000000..df1d66a --- /dev/null +++ b/integrations/woocommerce/kaasu-for-woocommerce/uninstall.php @@ -0,0 +1,5 @@ + Date: Sat, 24 Oct 2026 00:04:59 +0530 Subject: [PATCH 3/3] ci(woocommerce): fix duplicate version comment --- .github/workflows/woocommerce.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/woocommerce.yml b/.github/workflows/woocommerce.yml index 78a064d..bcc9d12 100644 --- a/.github/workflows/woocommerce.yml +++ b/.github/workflows/woocommerce.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - - uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # v2 # v2 + - uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # v2 with: { php-version: '8.2' } - run: find integrations/woocommerce -name '*.php' -print0 | xargs -0 -n1 php -l - run: php integrations/woocommerce/tests/run.php