diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 3792af4b..e12c657b 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,10 +6,14 @@ #include "generated_plugin_registrant.h" +#include #include #include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); + file_selector_plugin_register_with_registrar(file_selector_linux_registrar); g_autoptr(FlPluginRegistrar) gtk_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin"); gtk_plugin_register_with_registrar(gtk_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 5d074230..44535824 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + file_selector_linux gtk url_launcher_linux ) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index e996361c..90b66f5c 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,6 +6,7 @@ import FlutterMacOS import Foundation import app_links +import file_selector_macos import flutter_local_notifications import geolocator_apple import mobile_scanner @@ -16,6 +17,7 @@ import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) + FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin")) MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) diff --git a/pubspec.lock b/pubspec.lock index bc6b3aad..b7fb3b66 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -129,6 +129,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.15.0" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937" + url: "https://pub.dev" + source: hosted + version: "0.3.5+2" crypto: dependency: transitive description: @@ -217,6 +225,38 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.1" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0" + url: "https://pub.dev" + source: hosted + version: "0.9.4" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a" + url: "https://pub.dev" + source: hosted + version: "0.9.5" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85" + url: "https://pub.dev" + source: hosted + version: "2.7.0" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd" + url: "https://pub.dev" + source: hosted + version: "0.9.3+5" fixnum: dependency: transitive description: @@ -294,6 +334,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.7" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: "38d1c268de9097ff59cf0e844ac38759fc78f76836d37edad06fa21e182055a0" + url: "https://pub.dev" + source: hosted + version: "2.0.34" flutter_stripe: dependency: "direct main" description: @@ -504,6 +552,70 @@ packages: url: "https://pub.dev" source: hosted version: "4.5.4" + image_picker: + dependency: "direct main" + description: + name: image_picker + sha256: "784210112be18ea55f69d7076e2c656a4e24949fa9e76429fe53af0c0f4fa320" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: "66810af8e99b2657ee98e5c6f02064f69bb63f7a70e343937f70946c5f8c6622" + url: "https://pub.dev" + source: hosted + version: "0.8.13+16" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: b9c4a438a9ff4f60808c9cf0039b93a42bb6c2211ef6ebb647394b2b3fa84588 + url: "https://pub.dev" + source: hosted + version: "0.8.13+6" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4" + url: "https://pub.dev" + source: hosted + version: "0.2.2" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91" + url: "https://pub.dev" + source: hosted + version: "0.2.2+1" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c" + url: "https://pub.dev" + source: hosted + version: "2.11.1" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae + url: "https://pub.dev" + source: hosted + version: "0.2.2" intl: dependency: "direct main" description: @@ -1374,5 +1486,5 @@ packages: source: hosted version: "2.1.0" sdks: - dart: ">=3.9.2 <4.0.0" - flutter: ">=3.35.0" + dart: ">=3.10.0 <4.0.0" + flutter: ">=3.38.0" diff --git a/supabase/functions/approveRefund/index.ts b/supabase/functions/approveRefund/index.ts index 77848ef9..47b26536 100644 --- a/supabase/functions/approveRefund/index.ts +++ b/supabase/functions/approveRefund/index.ts @@ -5,19 +5,29 @@ import { processRefund } from "./logic.ts"; const corsHeaders = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, POST, OPTIONS", - "Access-Control-Allow-Headers": "Content-Type, Authorization, apikey", + "Access-Control-Allow-Headers": "Content-Type, Authorization, apikey, x-client-info", }; serve(async (req) => { if (req.method === "OPTIONS") { - return new Response(null, { status: 200, headers: corsHeaders }); + return new Response(null, { status: 204, headers: corsHeaders }); } +let userId: string, transactionId: string, amount: string, note: string; + try { - const url = new URL(req.url); - const userId = url.searchParams.get("user_id") || ""; - const transactionId = url.searchParams.get("transaction_id") || ""; - const amount = url.searchParams.get("amount") || ""; + const body = await req.json(); + userId = body.customerId || body.user_id; + transactionId = body.id || body.transactionId || body.transaction_id; + amount = body.amount; + note = body.note; + + if (!userId || !transactionId || !amount) { + return new Response( + JSON.stringify({ error: "Missing required parameters" }), + { status: 400, headers: corsHeaders } + ); + } const supabaseUrl = Deno.env.get("SUPABASE_URL"); const serviceKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY"); @@ -35,10 +45,10 @@ serve(async (req) => { }); const deps = { - updateRefund: async (transactionId: string) => { + updateRefund: async (transactionId: string, note: string) => { const { error } = await supabase .from("Refunds") - .update({ status: "approved" }) + .update({ status: "approved", "admin-note": note }) .eq("transaction_id", transactionId); if (error) throw new Error(error.message); @@ -70,7 +80,8 @@ serve(async (req) => { sendEmail: async ( email: string, transactionId: string, - amount: string + amount: string, + note: string ) => { const response = await fetch( "https://api.resend.com/emails", @@ -89,6 +100,7 @@ serve(async (req) => {

Your refund for transaction ${transactionId} was approved.

$${amount} has been added to your loyalty card.

+

${note ? `Note: ${note}` : ""}

`, }), } @@ -101,7 +113,7 @@ serve(async (req) => { }; const result = await processRefund( - { userId, transactionId, amount }, + { userId, transactionId, amount, note }, deps ); @@ -115,6 +127,7 @@ serve(async (req) => { } ); } catch (error) { + console.log(error); return new Response( JSON.stringify({ error: error.message }), { diff --git a/supabase/functions/approveRefund/logic.test.ts b/supabase/functions/approveRefund/logic.test.ts index 7c6b677d..9d9733f9 100644 --- a/supabase/functions/approveRefund/logic.test.ts +++ b/supabase/functions/approveRefund/logic.test.ts @@ -7,34 +7,35 @@ import { function createMockDeps(overrides: Partial = {}) { return { - updateRefund: async (_: string) => {}, + updateRefund: async (_: string, __: string) => {}, getUserEmail: async (_: string) => "test@example.com", incrementLoyalty: async (_: string, __: number) => {}, - sendEmail: async (_: string, __: string, ___: string) => {}, + sendEmail: async (_: string, __: string, ___: string, ____: string) => {}, ...overrides, }; } - + Deno.test("processRefund succeeds with valid input", async () => { const deps = createMockDeps(); - + const result = await processRefund( { userId: "user1", transactionId: "tx1", amount: "25", + note: "Approved by admin", }, deps ); - + assertEquals(result.success, true); assertEquals(result.transactionId, "tx1"); assertEquals(result.amount, "25"); }); - + Deno.test("throws if params are missing", async () => { const deps = createMockDeps(); - + await assertRejects( () => processRefund( @@ -42,6 +43,7 @@ import { userId: "", transactionId: "tx1", amount: "25", + note: "Test note", }, deps ), @@ -49,14 +51,14 @@ import { "Missing params" ); }); - + Deno.test("propagates updateRefund error", async () => { const deps = createMockDeps({ updateRefund: async () => { throw new Error("DB failure"); }, }); - + await assertRejects( () => processRefund( @@ -64,6 +66,7 @@ import { userId: "user1", transactionId: "tx1", amount: "25", + note: "Test note", }, deps ), @@ -71,12 +74,12 @@ import { "DB failure" ); }); - + Deno.test("throws if user email not found", async () => { const deps = createMockDeps({ getUserEmail: async () => "", }); - + await assertRejects( () => processRefund( @@ -84,6 +87,7 @@ import { userId: "user1", transactionId: "tx1", amount: "25", + note: "Test note", }, deps ), @@ -91,14 +95,14 @@ import { "User email not found" ); }); - + Deno.test("propagates incrementLoyalty error", async () => { const deps = createMockDeps({ incrementLoyalty: async () => { throw new Error("RPC failed"); }, }); - + await assertRejects( () => processRefund( @@ -106,6 +110,7 @@ import { userId: "user1", transactionId: "tx1", amount: "25", + note: "Test note", }, deps ), @@ -113,14 +118,14 @@ import { "RPC failed" ); }); - + Deno.test("propagates sendEmail error", async () => { const deps = createMockDeps({ sendEmail: async () => { throw new Error("Email failed"); }, }); - + await assertRejects( () => processRefund( @@ -128,6 +133,7 @@ import { userId: "user1", transactionId: "tx1", amount: "25", + note: "Test note", }, deps ), diff --git a/supabase/functions/approveRefund/logic.ts b/supabase/functions/approveRefund/logic.ts index 40de0543..cfc63708 100644 --- a/supabase/functions/approveRefund/logic.ts +++ b/supabase/functions/approveRefund/logic.ts @@ -1,27 +1,28 @@ export interface RefundDependencies { - updateRefund: (transactionId: string) => Promise; + updateRefund: (transactionId: string, note: string) => Promise; getUserEmail: (userId: string) => Promise; incrementLoyalty: (userId: string, amount: number) => Promise; - sendEmail: (email: string, transactionId: string, amount: string) => Promise; + sendEmail: (email: string, transactionId: string, amount: string, note: string) => Promise; } export interface RefundParams { userId: string; transactionId: string; amount: string; + note: string; } export async function processRefund( params: RefundParams, deps: RefundDependencies ) { - const { userId, transactionId, amount } = params; + const { userId, transactionId, amount, note } = params; if (!userId || !transactionId || !amount) { throw new Error("Missing params"); } - await deps.updateRefund(transactionId); + await deps.updateRefund(transactionId, note); const email = await deps.getUserEmail(userId); @@ -31,7 +32,7 @@ export interface RefundDependencies { await deps.incrementLoyalty(userId, Number(amount)); - await deps.sendEmail(email, transactionId, amount); + await deps.sendEmail(email, transactionId, amount, note); return { success: true, diff --git a/supabase/functions/denyRefund/index.ts b/supabase/functions/denyRefund/index.ts index d296c0b5..47d5c830 100644 --- a/supabase/functions/denyRefund/index.ts +++ b/supabase/functions/denyRefund/index.ts @@ -5,9 +5,10 @@ import { handleDenyRefund, sendDenialEmail } from "./logic.ts"; const CORS_HEADERS = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, POST, OPTIONS", - "Access-Control-Allow-Headers": "Content-Type, Authorization, apikey", + "Access-Control-Allow-Headers": "Content-Type, Authorization, apikey, x-client-info", }; + serve(async (req) => { if (req.method === "OPTIONS") { return new Response(null, { status: 200, headers: CORS_HEADERS }); @@ -31,8 +32,8 @@ serve(async (req) => { const response = await handleDenyRefund(req, { supabase, - sendEmail: (to, transactionId, amount) => - sendDenialEmail(resendKey!, to, transactionId, amount), + sendEmail: (to, transactionId, amount, note) => + sendDenialEmail(resendKey!, to, transactionId, amount, note), }); return new Response(response.body, { diff --git a/supabase/functions/denyRefund/logic.test.ts b/supabase/functions/denyRefund/logic.test.ts index 4b2e6c33..bc8f239a 100644 --- a/supabase/functions/denyRefund/logic.test.ts +++ b/supabase/functions/denyRefund/logic.test.ts @@ -10,23 +10,24 @@ import { sendDenialEmail } from "./logic.ts"; - function makeUrl(params: Record) { - const url = new URL("http://localhost/deny-refund"); - for (const [k, v] of Object.entries(params)) { - url.searchParams.set(k, v); - } - return url; + function makeRequestWithBody(body: Record) { + return new Request("http://localhost/deny-refund", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); } - - function makeFullUrl(overrides: Partial> = {}) { - return makeUrl({ + + function makeFullRequest(overrides: Partial> = {}) { + return makeRequestWithBody({ user_id: "user-123", transaction_id: "txn-abc", amount: "25.00", + note: "Policy violation", ...overrides, }); } - + function makeSupabaseMock(overrides: { updateError?: { message: string } | null; user?: { id: string; email?: string } | null; @@ -50,10 +51,6 @@ import { }, } as any; } - - function makeRequest(url: URL) { - return new Request(url.toString(), { method: "GET" }); - } function mockFetch(ok: boolean, responseText = "") { globalThis.fetch = () => @@ -66,71 +63,56 @@ import { function restoreFetch() { globalThis.fetch = fetch; } - - Deno.test("extractParams — returns all three params when present", () => { - const url = makeFullUrl(); - const params = extractParams(url); - + + Deno.test("extractParams — returns all four params when present", async () => { + const req = makeFullRequest(); + const params = await extractParams(req); + assertEquals(params.userId, "user-123"); assertEquals(params.transactionId, "txn-abc"); assertEquals(params.amount, "25.00"); + assertEquals(params.note, "Policy violation"); }); - - Deno.test("extractParams — throws when user_id is missing", () => { - const url = makeUrl({ transaction_id: "txn-abc", amount: "25.00" }); - try { - extractParams(url); - } catch (e) { - assertEquals(e instanceof Error, true); - assertEquals((e as Error).message, "Missing params"); - } - }); - - Deno.test("extractParams — throws when transaction_id is missing", () => { - const url = makeUrl({ user_id: "user-123", amount: "25.00" }); - try { - extractParams(url); - } catch (e) { - assertEquals(e instanceof Error, true); - assertEquals((e as Error).message, "Missing params"); - } - }); - - Deno.test("extractParams — throws when amount is missing", () => { - const url = makeUrl({ user_id: "user-123", transaction_id: "txn-abc" }); - try { - extractParams(url); - } catch (e) { - assertEquals(e instanceof Error, true); - assertEquals((e as Error).message, "Missing params"); - } + + + Deno.test("extractParams — allows missing note parameter", async () => { + const req = makeRequestWithBody({ user_id: "user-123", transaction_id: "txn-abc", amount: "25.00" }); + const params = await extractParams(req); + + assertEquals(params.userId, "user-123"); + assertEquals(params.transactionId, "txn-abc"); + assertEquals(params.amount, "25.00"); + assertEquals(params.note, undefined); }); - - Deno.test("extractParams — throws when all params are missing", () => { - const url = new URL("http://localhost/deny-refund"); - try { - extractParams(url); - } catch (e) { - assertEquals(e instanceof Error, true); - assertEquals((e as Error).message, "Missing params"); - } + + Deno.test("extractParams — throws when body is not valid JSON", async () => { + const req = new Request("http://localhost/deny-refund", { + method: "POST", + body: "not valid json", + }); + + await assertRejects( + () => extractParams(req), + Error, + "Invalid JSON body" + ); }); - + Deno.test("denyRefundInDb — resolves without error on success", async () => { const supabase = makeSupabaseMock(); - await denyRefundInDb(supabase, "txn-abc"); + await denyRefundInDb(supabase, "txn-abc", "Policy violation"); }); - + Deno.test("denyRefundInDb — throws with prefixed message on Supabase error", async () => { const supabase = makeSupabaseMock({ updateError: { message: "row not found" } }); - + await assertRejects( - () => denyRefundInDb(supabase, "txn-abc"), + () => denyRefundInDb(supabase, "txn-abc", "Policy violation"), Error, "Refund update error: row not found" ); }); - + Deno.test("denyRefundInDb — passes correct transaction_id to Supabase", async () => { let capturedVal: string | undefined; const supabase = { @@ -143,94 +125,81 @@ import { }), }), } as any; - - await denyRefundInDb(supabase, "txn-xyz"); + + await denyRefundInDb(supabase, "txn-xyz", "Policy violation"); assertEquals(capturedVal, "txn-xyz"); }); - + Deno.test("getUserEmail — returns email on valid user", async () => { const supabase = makeSupabaseMock({ user: { id: "user-123", email: "user@example.com" } }); - + const email = await getUserEmail(supabase, "user-123"); assertEquals(email, "user@example.com"); }); - + Deno.test("getUserEmail — throws when user is null", async () => { const supabase = makeSupabaseMock({ user: null }); - + await assertRejects( () => getUserEmail(supabase, "ghost"), Error, "User not found" ); }); - + Deno.test("getUserEmail — throws when Supabase returns an error", async () => { const supabase = makeSupabaseMock({ userError: { message: "JWT invalid" }, user: null }); - + await assertRejects( () => getUserEmail(supabase, "user-123"), Error, "User not found: JWT invalid" ); }); - + Deno.test("getUserEmail — throws when user has no email", async () => { - const supabase = makeSupabaseMock({ user: { id: "user-123" } }); - + const supabase = makeSupabaseMock({ user: { id: "user-123" } }); + await assertRejects( () => getUserEmail(supabase, "user-123"), Error, "User email not found" ); }); - + Deno.test("handleDenyRefund — returns 200 with confirmation HTML on success", async () => { - const req = makeRequest(makeFullUrl()); - const sendEmail = (_to: string, _txn: string, _amt: string) => Promise.resolve(); - + const req = makeFullRequest(); + const sendEmail = (_to: string, _txn: string, _amt: string, _note: string) => Promise.resolve(); + const res = await handleDenyRefund(req, { supabase: makeSupabaseMock(), sendEmail, }); const body = await res.text(); - + assertEquals(res.status, 200); assertEquals(body.includes("txn-abc"), true); assertEquals(body.includes("25.00"), true); }); - + Deno.test("handleDenyRefund — calls sendEmail with correct args", async () => { - const req = makeRequest(makeFullUrl()); - let capturedArgs: [string, string, string] | undefined; - + const req = makeFullRequest(); + let capturedArgs: [string, string, string, string] | undefined; + await handleDenyRefund(req, { supabase: makeSupabaseMock(), - sendEmail: (to, txn, amt) => { - capturedArgs = [to, txn, amt]; + sendEmail: (to, txn, amt, note) => { + capturedArgs = [to, txn, amt, note]; return Promise.resolve(); }, }); - - assertEquals(capturedArgs, ["user@example.com", "txn-abc", "25.00"]); - }); - - Deno.test("handleDenyRefund — throws Missing params when query params absent", async () => { - const req = new Request("http://localhost/deny-refund", { method: "GET" }); - - await assertRejects( - () => handleDenyRefund(req, { - supabase: makeSupabaseMock(), - sendEmail: () => Promise.resolve(), - }), - Error, - "Missing params" - ); + + assertEquals(capturedArgs, ["user@example.com", "txn-abc", "25.00", "Policy violation"]); }); - + Deno.test("handleDenyRefund — throws when DB update fails", async () => { - const req = makeRequest(makeFullUrl()); - + const req = makeFullRequest(); + await assertRejects( () => handleDenyRefund(req, { supabase: makeSupabaseMock({ updateError: { message: "constraint violation" } }), @@ -240,10 +209,10 @@ import { "Refund update error: constraint violation" ); }); - + Deno.test("handleDenyRefund — throws when user is not found", async () => { - const req = makeRequest(makeFullUrl()); - + const req = makeFullRequest(); + await assertRejects( () => handleDenyRefund(req, { supabase: makeSupabaseMock({ user: null }), @@ -253,10 +222,10 @@ import { "User not found" ); }); - + Deno.test("handleDenyRefund — throws when sendEmail fails", async () => { - const req = makeRequest(makeFullUrl()); - + const req = makeFullRequest(); + await assertRejects( () => handleDenyRefund(req, { supabase: makeSupabaseMock(), @@ -270,17 +239,17 @@ import { Deno.test("sendDenialEmail — resolves without error on success", async () => { mockFetch(true); try { - await sendDenialEmail("test-api-key", "user@example.com", "txn-abc", "25.00"); + await sendDenialEmail("test-api-key", "user@example.com", "txn-abc", "25.00", "Policy violation"); } finally { restoreFetch(); } }); - + Deno.test("sendDenialEmail — throws with error text when response is not ok", async () => { mockFetch(false, "Invalid API key"); try { await assertRejects( - () => sendDenialEmail("bad-key", "user@example.com", "txn-abc", "25.00"), + () => sendDenialEmail("bad-key", "user@example.com", "txn-abc", "25.00", "Policy violation"), Error, "Email send failed: Invalid API key" ); @@ -288,61 +257,62 @@ import { restoreFetch(); } }); - + Deno.test("sendDenialEmail — sends POST to the correct Resend endpoint", async () => { let capturedUrl: string | undefined; let capturedInit: RequestInit | undefined; - + globalThis.fetch = (url: string | URL | Request, init?: RequestInit) => { capturedUrl = url.toString(); capturedInit = init; return Promise.resolve({ ok: true, text: () => Promise.resolve("") } as Response); }; - + try { - await sendDenialEmail("test-key", "user@example.com", "txn-abc", "25.00"); + await sendDenialEmail("test-key", "user@example.com", "txn-abc", "25.00", "Policy violation"); } finally { restoreFetch(); } - + assertEquals(capturedUrl, "https://api.resend.com/emails"); assertEquals(capturedInit?.method, "POST"); }); - + Deno.test("sendDenialEmail — sends correct Authorization header", async () => { let capturedHeaders: Record | undefined; - + globalThis.fetch = (_url: string | URL | Request, init?: RequestInit) => { capturedHeaders = init?.headers as Record; return Promise.resolve({ ok: true, text: () => Promise.resolve("") } as Response); }; - + try { - await sendDenialEmail("my-resend-key", "user@example.com", "txn-abc", "25.00"); + await sendDenialEmail("my-resend-key", "user@example.com", "txn-abc", "25.00", "Policy violation"); } finally { restoreFetch(); } - + assertEquals(capturedHeaders?.["Authorization"], "Bearer my-resend-key"); assertEquals(capturedHeaders?.["Content-Type"], "application/json"); }); - - Deno.test("sendDenialEmail — sends correct recipient, transactionId, and amount in body", async () => { + + Deno.test("sendDenialEmail — sends correct recipient, transactionId, amount, and note in body", async () => { let capturedBody: any; - + globalThis.fetch = (_url: string | URL | Request, init?: RequestInit) => { capturedBody = JSON.parse(init?.body as string); return Promise.resolve({ ok: true, text: () => Promise.resolve("") } as Response); }; - + try { - await sendDenialEmail("test-key", "customer@example.com", "txn-xyz", "49.99"); + await sendDenialEmail("test-key", "customer@example.com", "txn-xyz", "49.99", "Custom note here"); } finally { restoreFetch(); } - + assertEquals(capturedBody.to, "customer@example.com"); assertEquals(capturedBody.html.includes("txn-xyz"), true); assertEquals(capturedBody.html.includes("49.99"), true); + assertEquals(capturedBody.html.includes("Custom note here"), true); assertEquals(capturedBody.subject, "Refund Request Denied"); }); \ No newline at end of file diff --git a/supabase/functions/denyRefund/logic.ts b/supabase/functions/denyRefund/logic.ts index 5020cadb..d86e3db8 100644 --- a/supabase/functions/denyRefund/logic.ts +++ b/supabase/functions/denyRefund/logic.ts @@ -4,32 +4,41 @@ export interface DenyRefundParams { userId: string; transactionId: string; amount: string; + note: string; } export interface DenyRefundDeps { supabase: SupabaseClient; - sendEmail: (to: string, transactionId: string, amount: string) => Promise; + sendEmail: (to: string, transactionId: string, amount: string, note: string) => Promise; } -export function extractParams(url: URL): DenyRefundParams { - const userId = url.searchParams.get("user_id"); - const transactionId = url.searchParams.get("transaction_id"); - const amount = url.searchParams.get("amount"); +export async function extractParams(req: Request): Promise { + try { + const body = await req.json(); - if (!userId || !transactionId || !amount) { - throw new Error("Missing params"); - } + const userId = body.customerId || body.user_id; + const transactionId = body.id || body.transactionId || body.transaction_id; + const amount = body.amount; + const note = body.note; + + if (!userId || !transactionId || !amount) { + throw new Error("Missing params"); + } - return { userId, transactionId, amount }; + return { userId, transactionId, amount, note }; + } catch (err) { + throw new Error("Invalid JSON body"); + } } export async function denyRefundInDb( supabase: SupabaseClient, - transactionId: string + transactionId: string, + note: string ): Promise { const { error } = await supabase .from("Refunds") - .update({ status: "denied" }) + .update({ status: "denied", "admin-note": note }) .eq("transaction_id", transactionId); if (error) { @@ -61,7 +70,8 @@ export async function sendDenialEmail( resendApiKey: string, to: string, transactionId: string, - amount: string + amount: string, + note: string, ): Promise { const response = await fetch("https://api.resend.com/emails", { method: "POST", @@ -78,6 +88,7 @@ export async function sendDenialEmail(

Unfortunately, your refund request for transaction ${transactionId} has been denied.

Amount: $${amount}

If you have questions about this decision, please contact support.

+

${note ? `Note: ${note}` : ""}

`, }), }); @@ -92,21 +103,19 @@ export async function handleDenyRefund( req: Request, deps: DenyRefundDeps ): Promise { - const url = new URL(req.url); - const { userId, transactionId, amount } = extractParams(url); + const { userId, transactionId, amount, note } = await extractParams(req); - await denyRefundInDb(deps.supabase, transactionId); + await denyRefundInDb(deps.supabase, transactionId, note); const userEmail = await getUserEmail(deps.supabase, userId); - await deps.sendEmail(userEmail, transactionId, amount); + await deps.sendEmail(userEmail, transactionId, amount, note); const html = `Refund Denied - The refund request has been denied and - the customer has been notified via email. - Transaction: ${transactionId} - Amount: $${amount} - `; +The refund request has been denied and +the customer has been notified via email. +Transaction: ${transactionId} +Amount: $${amount}`; return new Response(html, { status: 200, diff --git a/supabase/functions/maintenance-request/index.ts b/supabase/functions/maintenance-request/index.ts new file mode 100644 index 00000000..1313ef9f --- /dev/null +++ b/supabase/functions/maintenance-request/index.ts @@ -0,0 +1,83 @@ +import { serve } from "https://deno.land/std@0.168.0/http/server.ts"; +import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'; + +const corsHeaders = { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type", + "Access-Control-Allow-Methods": "POST, OPTIONS", +}; + +serve(async (req: Request) => { + if (req.method === "OPTIONS") { + return new Response("ok", { + status: 200, + headers: corsHeaders, + }); + } + + try { + const supabaseUrl = Deno.env.get('SUPABASE_URL') ?? ''; + const supabaseKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''; + const supabase = createClient(supabaseUrl, supabaseKey); + + const body = await req.json(); + + const { user_id, category, description, image, location } = body; + + console.log(`Processing Maintenance Request for User: ${user_id} at Location: ${location}`); + + if (!user_id || !category || !description || !location) { + console.error("Missing required fields in request body."); + return new Response( + JSON.stringify({ error: "Missing required fields: user_id, category, description, or location" }), + { + status: 400, + headers: { ...corsHeaders, "Content-Type": "application/json" } + } + ); + } + + const { data, error: dbError } = await supabase + .from('Maintenance') + .insert([ + { + user_id: user_id, + category: category, + description: description, + location: location, + image_data: image, + created_at: new Date().toISOString() + } + ]) + .select(); + + if (dbError) { + console.error(`Database Error: ${dbError.message}`); + return new Response( + JSON.stringify({ error: "Database save failed", details: dbError.message }), + { + status: 500, + headers: { ...corsHeaders, "Content-Type": "application/json" } + } + ); + } + + return new Response( + JSON.stringify({ success: true, message: "Request logged", record: data }), + { + status: 200, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }, + ); + + } catch (err: any) { + console.error(`Global Handler Error: ${err.message}`); + return new Response( + JSON.stringify({ error: "Internal Server Error", message: err.message }), + { + status: 500, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }, + ); + } +}); \ No newline at end of file diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 3e035bd8..41c18a71 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -7,6 +7,7 @@ #include "generated_plugin_registrant.h" #include +#include #include #include #include @@ -14,6 +15,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { AppLinksPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("AppLinksPluginCApi")); + FileSelectorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FileSelectorWindows")); GeolocatorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("GeolocatorWindows")); PermissionHandlerWindowsPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index d49f9206..98923c86 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST app_links + file_selector_windows geolocator_windows permission_handler_windows url_launcher_windows