Merge pull request 'feat: add deposit with stripe (CAS-28)' (!23) from feat/stripe into main
Reviewed-on: https://git.simonis.lol/projects/casino/pulls/23 Reviewed-by: Huy <ptran@noreply@simonis.lol> Reviewed-by: jank1619 <jan@kjan.email>
This commit is contained in:
commit
702e54b5da
13 changed files with 185 additions and 2 deletions
|
@ -24,6 +24,7 @@ repositories {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
implementation("com.stripe:stripe-java:20.79.0")
|
||||
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
|
||||
implementation("org.springframework.boot:spring-boot-starter-web")
|
||||
compileOnly("org.projectlombok:lombok")
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
package de.szut.casino.deposit;
|
||||
|
||||
import com.stripe.Stripe;
|
||||
import com.stripe.exception.StripeException;
|
||||
import com.stripe.model.checkout.Session;
|
||||
import com.stripe.param.InvoiceItemCreateParams;
|
||||
import com.stripe.param.PriceCreateParams;
|
||||
import com.stripe.param.checkout.SessionCreateParams;
|
||||
import de.szut.casino.deposit.dto.AmountDto;
|
||||
import de.szut.casino.deposit.dto.SessionIdDto;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.cassandra.CassandraProperties;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
public class DepositController {
|
||||
|
||||
@Value("${stripe.secret.key}")
|
||||
private String stripeKey;
|
||||
|
||||
@PostMapping("/deposit/checkout")
|
||||
public ResponseEntity<SessionIdDto> checkout(
|
||||
@RequestBody @Valid AmountDto amountDto,
|
||||
@RequestHeader("Origin") String origin
|
||||
) throws StripeException {
|
||||
Stripe.apiKey = stripeKey;
|
||||
|
||||
SessionCreateParams params = SessionCreateParams.builder()
|
||||
.addLineItem(SessionCreateParams.LineItem.builder()
|
||||
.setAmount((long) amountDto.getAmount() * 100)
|
||||
.setCurrency("EUR")
|
||||
.setQuantity(1L)
|
||||
.setName("Einzahlung")
|
||||
.build())
|
||||
.setSuccessUrl(origin+"/deposit/success")
|
||||
.setMode(SessionCreateParams.Mode.PAYMENT)
|
||||
.build();
|
||||
|
||||
Session session = Session.create(params);
|
||||
|
||||
return ResponseEntity.ok(new SessionIdDto(session.getId()));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package de.szut.casino.deposit.dto;
|
||||
|
||||
import jakarta.validation.constraints.Min;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
@Setter
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class AmountDto {
|
||||
@Min(50)
|
||||
private double amount;
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package de.szut.casino.deposit.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
@Setter
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class SessionIdDto {
|
||||
private String sessionId;
|
||||
}
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
spring.datasource.url=jdbc:postgresql://${DB_HOST:-localhost}:5432/postgresdb
|
||||
spring.datasource.url=jdbc:postgresql://${DB_HOST:localhost}:5432/postgresdb
|
||||
spring.datasource.username=postgres_user
|
||||
spring.datasource.password=postgres_pass
|
||||
server.port=8080
|
||||
spring.jpa.hibernate.ddl-auto=create-drop
|
||||
|
||||
stripe.secret.key=${STRIPE_SECRET_KEY:sk_test_51QrePYIvCfqz7ANgqam8rEwWcMeKiLOof3j6SCMgu2sl4sESP45DJxca16mWcYo1sQaiBv32CMR6Z4AAAGQPCJo300ubuZKO8I}
|
||||
|
||||
spring.application.name=lf12_starter
|
||||
#client registration configuration
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
"@angular/platform-browser": "^18.2.0",
|
||||
"@angular/platform-browser-dynamic": "^18.2.0",
|
||||
"@angular/router": "^18.2.0",
|
||||
"@stripe/stripe-js": "^5.6.0",
|
||||
"@tailwindcss/postcss": "^4.0.3",
|
||||
"keycloak-angular": "^16.0.1",
|
||||
"keycloak-js": "^25.0.5",
|
||||
|
@ -530,6 +531,8 @@
|
|||
|
||||
"@socket.io/component-emitter": ["@socket.io/component-emitter@3.1.2", "", {}, "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="],
|
||||
|
||||
"@stripe/stripe-js": ["@stripe/stripe-js@5.6.0", "", {}, "sha512-w8CEY73X/7tw2KKlL3iOk679V9bWseE4GzNz3zlaYxcTjmcmWOathRb0emgo/QQ3eoNzmq68+2Y2gxluAv3xGw=="],
|
||||
|
||||
"@tailwindcss/node": ["@tailwindcss/node@4.0.3", "", { "dependencies": { "enhanced-resolve": "^5.18.0", "jiti": "^2.4.2", "tailwindcss": "4.0.3" } }, "sha512-QsVJokOl0pJ4AbJV33D2npvLcHGPWi5MOSZtrtE0GT3tSx+3D0JE2lokLA8yHS1x3oCY/3IyRyy7XX6tmzid7A=="],
|
||||
|
||||
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.0.3", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.0.3", "@tailwindcss/oxide-darwin-arm64": "4.0.3", "@tailwindcss/oxide-darwin-x64": "4.0.3", "@tailwindcss/oxide-freebsd-x64": "4.0.3", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.0.3", "@tailwindcss/oxide-linux-arm64-gnu": "4.0.3", "@tailwindcss/oxide-linux-arm64-musl": "4.0.3", "@tailwindcss/oxide-linux-x64-gnu": "4.0.3", "@tailwindcss/oxide-linux-x64-musl": "4.0.3", "@tailwindcss/oxide-win32-arm64-msvc": "4.0.3", "@tailwindcss/oxide-win32-x64-msvc": "4.0.3" } }, "sha512-FFcp3VNvRjjmFA39ORM27g2mbflMQljhvM7gxBAujHxUy4LXlKa6yMF9wbHdTbPqTONiCyyOYxccvJyVyI/XBg=="],
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
"@angular/platform-browser": "^18.2.0",
|
||||
"@angular/platform-browser-dynamic": "^18.2.0",
|
||||
"@angular/router": "^18.2.0",
|
||||
"@stripe/stripe-js": "^5.6.0",
|
||||
"@tailwindcss/postcss": "^4.0.3",
|
||||
"keycloak-angular": "^16.0.1",
|
||||
"keycloak-js": "^25.0.5",
|
||||
|
|
|
@ -2,6 +2,7 @@ import { Routes } from '@angular/router';
|
|||
import { LandingComponent } from './feature/landing/landing.component';
|
||||
import { HomeComponent } from './feature/home/home.component';
|
||||
import { authGuard } from './auth.guard';
|
||||
import { DepositComponent } from './deposit/deposit.component';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
|
@ -13,4 +14,9 @@ export const routes: Routes = [
|
|||
component: HomeComponent,
|
||||
canActivate: [authGuard],
|
||||
},
|
||||
{
|
||||
path: 'deposit',
|
||||
component: DepositComponent,
|
||||
canActivate: [authGuard],
|
||||
},
|
||||
];
|
||||
|
|
0
frontend/src/app/deposit/deposit.component.css
Normal file
0
frontend/src/app/deposit/deposit.component.css
Normal file
21
frontend/src/app/deposit/deposit.component.html
Normal file
21
frontend/src/app/deposit/deposit.component.html
Normal file
|
@ -0,0 +1,21 @@
|
|||
<form [formGroup]="form" class="max-w-md mx-auto p-4 bg-white shadow-md rounded-lg">
|
||||
<div *ngIf="errorMsg" class="mb-4 text-red-500">
|
||||
{{ errorMsg }}
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="amount" class="block text-sm font-medium text-gray-700">Betrag</label>
|
||||
<input
|
||||
type="number"
|
||||
id="amount"
|
||||
formControlName="amount"
|
||||
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm text-black bg-white"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
(click)="submit()"
|
||||
class="w-full bg-indigo-600 text-white py-2 px-4 rounded-md hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
||||
>
|
||||
Einzahlen
|
||||
</button>
|
||||
</form>
|
51
frontend/src/app/deposit/deposit.component.ts
Normal file
51
frontend/src/app/deposit/deposit.component.ts
Normal file
|
@ -0,0 +1,51 @@
|
|||
import { ChangeDetectionStrategy, Component, inject, OnInit } from '@angular/core';
|
||||
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { loadStripe, Stripe } from '@stripe/stripe-js';
|
||||
import { DepositService } from '../service/deposit.service';
|
||||
import { debounceTime } from 'rxjs';
|
||||
import { environment } from '../../environments/environment';
|
||||
import { NgIf } from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'app-deposit',
|
||||
standalone: true,
|
||||
imports: [ReactiveFormsModule, NgIf],
|
||||
templateUrl: './deposit.component.html',
|
||||
styleUrl: './deposit.component.css',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class DepositComponent implements OnInit {
|
||||
protected form!: FormGroup;
|
||||
protected errorMsg = '';
|
||||
private stripe: Stripe | null = null;
|
||||
private service: DepositService = inject(DepositService);
|
||||
|
||||
async ngOnInit() {
|
||||
this.form = new FormGroup({
|
||||
amount: new FormControl(50, [Validators.min(50)]),
|
||||
});
|
||||
|
||||
this.form.controls['amount'].valueChanges.pipe(debounceTime(1000)).subscribe((value) => {
|
||||
if (value < 50) {
|
||||
this.errorMsg = 'Minimum Einzahlungsbetrag ist 50€';
|
||||
}
|
||||
});
|
||||
|
||||
this.stripe = await loadStripe(environment.STRIPE_KEY);
|
||||
}
|
||||
|
||||
submit() {
|
||||
if (!this.stripe) {
|
||||
this.errorMsg = 'Ein Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.';
|
||||
return;
|
||||
}
|
||||
if (!this.form.valid) {
|
||||
this.errorMsg = 'Bitte geben Sie einen gültigen Betrag ein.';
|
||||
return;
|
||||
}
|
||||
|
||||
this.service.handleDeposit(this.form.value.amount as number).subscribe(({ sessionId }) => {
|
||||
this.stripe?.redirectToCheckout({ sessionId });
|
||||
});
|
||||
}
|
||||
}
|
14
frontend/src/app/service/deposit.service.ts
Normal file
14
frontend/src/app/service/deposit.service.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { inject, Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class DepositService {
|
||||
private http: HttpClient = inject(HttpClient);
|
||||
|
||||
handleDeposit(amount: number): Observable<{ sessionId: string }> {
|
||||
return this.http.post<{ sessionId: string }>('/backend/deposit/checkout', { amount });
|
||||
}
|
||||
}
|
4
frontend/src/environments/environment.ts
Normal file
4
frontend/src/environments/environment.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export const environment = {
|
||||
STRIPE_KEY:
|
||||
'pk_test_51QrePYIvCfqz7ANgMizBorPpVjJ8S6gcaL4yvcMQnVaKyReqcQ6jqaQEF7aDZbDu8rNVsTZrw8ABek4ToxQX7KZe00jpGh8naG',
|
||||
};
|
Reference in a new issue