diff options
Diffstat (limited to 'backend/api/src/stripe.zig')
| -rw-r--r-- | backend/api/src/stripe.zig | 181 |
1 files changed, 181 insertions, 0 deletions
diff --git a/backend/api/src/stripe.zig b/backend/api/src/stripe.zig new file mode 100644 index 0000000..87cef10 --- /dev/null +++ b/backend/api/src/stripe.zig @@ -0,0 +1,181 @@ +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 ); +} |
