const z = @import( "std" ); const db = @import( "db.zig" ); const req = @import( "req.zig" ); const net = @import( "net-util.zig" ); const zap = @import( "zap" ); const jwt = @import( "jwt" ); const mail = @import( "mail.zig" ); const config = @import( "config.zig" ); const u = struct { usingnamespace @import( "util.zig" ); usingnamespace @import( "userdefs.zig" ); usingnamespace @import( "user.zig" ); }; const ArenaAllocator = z.heap.ArenaAllocator; const ErrorRes = net.ErrorResponse; const Status = zap.StatusCode; const ArrayList = z.ArrayList; const OkRes = net.OkResponse; const Request = zap.Request; const JWT = jwt.JWT; const alloc = u.alloc; const memeql = z.mem.eql; pub const routes = .{ .@"create-payment-intent" = createPaymentIntent }; pub const SubLength = enum(u32) { week = 1, month = 2, year = 3, pub fn getTime( self: SubLength ) i64 { switch( self ) { .week => return 7 * 24 * 60 * 60 * 1000, .month => return 30 * 24 * 60 * 60 * 1000, .year => return 365 * 24 * 60 * 60 * 1000 } } pub fn getCostAmount( self: SubLength ) u32 { switch( self ) { .week => return 270, .month => return 1000, .year => return 10000 } } }; const StripeUrlParams = struct { amount: u32 = 1000, currency: []const u8 = "usd", description: []const u8 = "Axonbox premium subscription", payment_method: []const u8, confirm: []const u8 = "true", uuid: []const u8, return_url: []const u8 }; const StripeRequestParams = struct { paymentMethodId: []const u8, subLength: SubLength = .month, pub const @"getty.db" = u.@"json.ignore.unknown"; }; const StripeResponse = struct { status: []const u8, pub const @"getty.db" = u.@"json.ignore.unknown"; }; ///freed by caller fn formatStripeParams( params: StripeUrlParams ) []const u8 { const ret = z.fmt.allocPrint( alloc, "amount={d}¤cy={s}&description={s}&payment_method={s}&confirm={s}&metadata%5Buuid%5D={s}&return_url={s}", .{ params.amount, params.currency, params.description, params.payment_method, params.confirm, params.uuid, params.return_url } ) catch return ""; return ret; } fn formatReturnUrl( buf: []u8 ) []const u8 { return z.fmt.bufPrintZ( buf, "{s}/payment-success.html", .{ config.server_url } ) catch return ""; } fn getStripeKeyBearer() ![]const u8 { const file = try u.readFile( "../data/stripe_key.txt" ); defer alloc.free( file ); const buf = try z.fmt.allocPrint( alloc, "Bearer {s}", .{ file } ); return buf; } pub fn handleStripeResponse( user: *u.UserEntry, res: StripeResponse, r: zap.Request, length: SubLength ) void { const status = res.status; if( memeql( u8, status, "succeeded" ) ) { user.subscription_data = u.SubData{ .plan = "paid", .endTime = z.time.milliTimestamp() + length.getTime(), .reminded = false }; u.updateEntry( user.* ) catch { return net.sendJson( r, .internal_server_error, ErrorRes{ .msg = "failed to update user" } ); }; return net.sendJson( r, .ok, OkRes( .{ .msg = "success", .paymentIntent = res } ) ); } net.sendJson( r, .bad_request, ErrorRes{ .msg = "failed" } ); } ///route @/create-payment-intent pub fn createPaymentIntent( r: zap.Request ) void { if( net.handleInvalidPostReq( r ) ) return; u.checkDbOnThread() catch return; var token = u.tokenFromPostReq( r ) catch return; defer token.deinit(); const params = u.jsonParse( StripeRequestParams, r.body.? ) catch { return net.sendJson( r, .bad_request, ErrorRes{ .msg = "invalid request format" } ); }; defer params.deinit(); var user = u.getEntry( token.claims.uuid ) catch { return net.sendJson( r, .unauthorized, ErrorRes{ .msg = "user not found" } ); }; defer user.db_entry.?.deinit(); if( !memeql( u8, user.subscription_data.plan, "free" ) ) { return net.sendJson( r, .bad_request, ErrorRes{ .msg = "user already subscribed" } ); } const stripe_key = getStripeKeyBearer() catch { return net.sendJson( r, .internal_server_error, ErrorRes{ .msg = "internal server error" } ); }; defer alloc.free( stripe_key ); const headers = [_]z.http.Header{ .{ .name = "Authorization", .value = stripe_key } }; var buf: [2048]u8 = undefined; const body = formatStripeParams( .{ .amount = params.v.subLength.getCostAmount(), .payment_method = params.v.paymentMethodId, .uuid = user.uuid, .return_url = formatReturnUrl( &buf ) } ); defer alloc.free( body ); var res = req.send( .{ .url = "https://api.stripe.com/v1/payment_intents", .method = .POST, .headers = &headers, .body = body }, alloc ); defer res.deinit(); if( !res.ok ) { z.debug.print( "failed to create payment intent: {s}\n", .{ res.body.? } ); return net.sendJson( r, .internal_server_error, ErrorRes{ .msg = "failed to create payment intent" } ); } const stripe_json = u.jsonParse( StripeResponse, res.body.? ) catch { return net.sendJson( r, .internal_server_error, ErrorRes{ .msg = "failed to parse provider response" } ); }; defer stripe_json.deinit(); handleStripeResponse( &user, stripe_json.v, r, params.v.subLength ); }