+ google drive
This commit is contained in:
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
@@ -8,6 +8,7 @@ data class SpaceDTO(
|
|||||||
val name: String,
|
val name: String,
|
||||||
val owner: UserDTO,
|
val owner: UserDTO,
|
||||||
val participants: Set<UserDTO> = emptySet(),
|
val participants: Set<UserDTO> = emptySet(),
|
||||||
|
val isGoogleDriveConnected: Boolean = false,
|
||||||
val createdBy: UserDTO? = null,
|
val createdBy: UserDTO? = null,
|
||||||
val createdAt: Instant,
|
val createdAt: Instant,
|
||||||
var updatedBy: UserDTO? = null,
|
var updatedBy: UserDTO? = null,
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ data class UserDTO(
|
|||||||
var tgId: Long? = null,
|
var tgId: Long? = null,
|
||||||
var tgUserName: String? = null,
|
var tgUserName: String? = null,
|
||||||
var photoUrl: String? = null,
|
var photoUrl: String? = null,
|
||||||
var roles: List<String>
|
var roles: List<String>,
|
||||||
|
var isGoogleDriveConnected: Boolean = false
|
||||||
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ object SpaceMapper {
|
|||||||
name = this.name,
|
name = this.name,
|
||||||
owner = this.owner.toDto(),
|
owner = this.owner.toDto(),
|
||||||
participants = this.participants.map { it.toDto() }.toSet(),
|
participants = this.participants.map { it.toDto() }.toSet(),
|
||||||
|
isGoogleDriveConnected = this.owner.googleRefreshToken != null,
|
||||||
createdBy = this.createdBy?.toDto(),
|
createdBy = this.createdBy?.toDto(),
|
||||||
createdAt = this.createdAt ?: throw IllegalArgumentException("createdAt is not provided"),
|
createdAt = this.createdAt ?: throw IllegalArgumentException("createdAt is not provided"),
|
||||||
updatedBy = this.updatedBy?.toDto(),
|
updatedBy = this.updatedBy?.toDto(),
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ object UserMapper {
|
|||||||
tgId = this.tgId,
|
tgId = this.tgId,
|
||||||
tgUserName = this.tgUserName,
|
tgUserName = this.tgUserName,
|
||||||
photoUrl = this.photoUrl,
|
photoUrl = this.photoUrl,
|
||||||
roles = this.roles
|
roles = this.roles,
|
||||||
|
isGoogleDriveConnected = this.googleRefreshToken != null
|
||||||
)
|
)
|
||||||
|
|
||||||
fun TelegramAuthDTO.toTelegramMap(): Map<String, String> =
|
fun TelegramAuthDTO.toTelegramMap(): Map<String, String> =
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ class SpaceRepoImpl(
|
|||||||
rs.getString("s_owner_username"),
|
rs.getString("s_owner_username"),
|
||||||
rs.getString("s_owner_firstname"),
|
rs.getString("s_owner_firstname"),
|
||||||
tgId = rs.getLong("s_owner_tg_id"),
|
tgId = rs.getLong("s_owner_tg_id"),
|
||||||
|
googleRefreshToken = rs.getString("s_owner_google_refresh_token")
|
||||||
),
|
),
|
||||||
participant = User(rs.getInt("sp_uid"), rs.getString("sp_username"), rs.getString("sp_first_name")),
|
participant = User(rs.getInt("sp_uid"), rs.getString("sp_username"), rs.getString("sp_first_name")),
|
||||||
createdAt = rs.getTimestamp("s_created_at").toInstant(),
|
createdAt = rs.getTimestamp("s_created_at").toInstant(),
|
||||||
@@ -111,6 +112,7 @@ class SpaceRepoImpl(
|
|||||||
ou.username as s_owner_username,
|
ou.username as s_owner_username,
|
||||||
ou.first_name as s_owner_firstname,
|
ou.first_name as s_owner_firstname,
|
||||||
ou.tg_id as s_owner_tg_id,
|
ou.tg_id as s_owner_tg_id,
|
||||||
|
ou.google_refresh_token as s_owner_google_refresh_token,
|
||||||
sp.participants_id as sp_uid,
|
sp.participants_id as sp_uid,
|
||||||
u.username as sp_username,
|
u.username as sp_username,
|
||||||
u.first_name as sp_first_name,
|
u.first_name as sp_first_name,
|
||||||
@@ -131,7 +133,7 @@ class SpaceRepoImpl(
|
|||||||
left join finance.users uau on s.updated_by_id = uau.id
|
left join finance.users uau on s.updated_by_id = uau.id
|
||||||
left join finance.transactions t on t.space_id = s.id
|
left join finance.transactions t on t.space_id = s.id
|
||||||
where s.is_deleted = false and t.created_at >= :lastRun
|
where s.is_deleted = false and t.created_at >= :lastRun
|
||||||
group by s.id, ou.username, ou.first_name, ou.tg_id, sp.participants_id, u.username, u.first_name, cau.username, cau.first_name,
|
group by s.id, ou.username, ou.first_name, ou.tg_id, ou.google_refresh_token, sp.participants_id, u.username, u.first_name, cau.username, cau.first_name,
|
||||||
uau.username, uau.first_name;
|
uau.username, uau.first_name;
|
||||||
""".trimMargin()
|
""".trimMargin()
|
||||||
val params = mapOf("lastRun" to lastRun)
|
val params = mapOf("lastRun" to lastRun)
|
||||||
@@ -150,6 +152,7 @@ class SpaceRepoImpl(
|
|||||||
ou.username as s_owner_username,
|
ou.username as s_owner_username,
|
||||||
ou.first_name as s_owner_firstname,
|
ou.first_name as s_owner_firstname,
|
||||||
ou.tg_id as s_owner_tg_id,
|
ou.tg_id as s_owner_tg_id,
|
||||||
|
ou.google_refresh_token as s_owner_google_refresh_token,
|
||||||
sp.participants_id as sp_uid,
|
sp.participants_id as sp_uid,
|
||||||
u.username as sp_username,
|
u.username as sp_username,
|
||||||
u.first_name as sp_first_name,
|
u.first_name as sp_first_name,
|
||||||
@@ -171,7 +174,7 @@ class SpaceRepoImpl(
|
|||||||
where (s.owner_id = :user_id
|
where (s.owner_id = :user_id
|
||||||
or sp.participants_id = :user_id)
|
or sp.participants_id = :user_id)
|
||||||
and s.is_deleted = false
|
and s.is_deleted = false
|
||||||
group by s.id, ou.username, ou.first_name, ou.tg_id, sp.participants_id, u.username, u.first_name, cau.username, cau.first_name,
|
group by s.id, ou.username, ou.first_name, ou.tg_id, ou.google_refresh_token, sp.participants_id, u.username, u.first_name, cau.username, cau.first_name,
|
||||||
uau.username, uau.first_name;
|
uau.username, uau.first_name;
|
||||||
""".trimMargin()
|
""".trimMargin()
|
||||||
val params = mapOf(
|
val params = mapOf(
|
||||||
@@ -191,6 +194,7 @@ class SpaceRepoImpl(
|
|||||||
ou.username as s_owner_username,
|
ou.username as s_owner_username,
|
||||||
ou.first_name as s_owner_firstname,
|
ou.first_name as s_owner_firstname,
|
||||||
ou.tg_id as s_owner_tg_id,
|
ou.tg_id as s_owner_tg_id,
|
||||||
|
ou.google_refresh_token as s_owner_google_refresh_token,
|
||||||
sp.participants_id as sp_uid,
|
sp.participants_id as sp_uid,
|
||||||
u.username as sp_username,
|
u.username as sp_username,
|
||||||
u.first_name as sp_first_name,
|
u.first_name as sp_first_name,
|
||||||
@@ -212,7 +216,7 @@ from finance.spaces s
|
|||||||
where (s.owner_id = :user_id
|
where (s.owner_id = :user_id
|
||||||
or sp.participants_id = :user_id)
|
or sp.participants_id = :user_id)
|
||||||
and s.is_deleted = false and s.id = :spaceId
|
and s.is_deleted = false and s.id = :spaceId
|
||||||
group by s.id, ou.username, ou.first_name, ou.tg_id, sp.participants_id, u.username, u.first_name, cau.username, cau.first_name,
|
group by s.id, ou.username, ou.first_name, ou.tg_id, ou.google_refresh_token, sp.participants_id, u.username, u.first_name, cau.username, cau.first_name,
|
||||||
uau.username, uau.first_name;
|
uau.username, uau.first_name;
|
||||||
""".trimMargin()
|
""".trimMargin()
|
||||||
val params = mapOf(
|
val params = mapOf(
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package space.luminic.finance.services
|
||||||
|
|
||||||
|
import com.google.api.client.auth.oauth2.AuthorizationCodeFlow
|
||||||
|
import com.google.api.client.auth.oauth2.BearerToken
|
||||||
|
import com.google.api.client.auth.oauth2.Credential
|
||||||
|
import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow
|
||||||
|
import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets
|
||||||
|
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential
|
||||||
|
import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport
|
||||||
|
import com.google.api.client.http.ByteArrayContent
|
||||||
|
import com.google.api.client.json.gson.GsonFactory
|
||||||
|
import com.google.api.services.drive.Drive
|
||||||
|
import com.google.api.services.drive.DriveScopes
|
||||||
|
import com.google.api.services.drive.model.File
|
||||||
|
import org.springframework.beans.factory.annotation.Value
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class GoogleDriveService(
|
||||||
|
@Value("\${google.client-id:}") private val clientId: String,
|
||||||
|
@Value("\${google.client-secret:}") private val clientSecret: String,
|
||||||
|
@Value("\${google.redirect-uri:}") private val redirectUri: String
|
||||||
|
) {
|
||||||
|
private val jsonFactory = GsonFactory.getDefaultInstance()
|
||||||
|
private val httpTransport = GoogleNetHttpTransport.newTrustedTransport()
|
||||||
|
|
||||||
|
fun exchangeCodeForRefreshToken(authCode: String): String {
|
||||||
|
if (clientId.isEmpty() || clientSecret.isEmpty()) return ""
|
||||||
|
|
||||||
|
val flow = GoogleAuthorizationCodeFlow.Builder(
|
||||||
|
httpTransport,
|
||||||
|
jsonFactory,
|
||||||
|
clientId,
|
||||||
|
clientSecret,
|
||||||
|
listOf(DriveScopes.DRIVE_FILE)
|
||||||
|
)
|
||||||
|
.setAccessType("offline")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val response = flow.newTokenRequest(authCode).setRedirectUri(redirectUri).execute()
|
||||||
|
return response.refreshToken ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
fun uploadExcelFile(refreshToken: String, fileName: String, content: ByteArray) {
|
||||||
|
if (refreshToken.isEmpty() || clientId.isEmpty() || clientSecret.isEmpty()) return
|
||||||
|
|
||||||
|
val credential = GoogleCredential.Builder()
|
||||||
|
.setTransport(httpTransport)
|
||||||
|
.setJsonFactory(jsonFactory)
|
||||||
|
.setClientSecrets(clientId, clientSecret)
|
||||||
|
.build()
|
||||||
|
.setRefreshToken(refreshToken)
|
||||||
|
|
||||||
|
val driveService = Drive.Builder(httpTransport, jsonFactory, credential)
|
||||||
|
.setApplicationName("Luminic Space")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val fileMetadata = File()
|
||||||
|
fileMetadata.name = fileName
|
||||||
|
fileMetadata.mimeType = "application/vnd.google-apps.spreadsheet"
|
||||||
|
|
||||||
|
val mediaContent = ByteArrayContent("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", content)
|
||||||
|
|
||||||
|
driveService.files().create(fileMetadata, mediaContent)
|
||||||
|
.setFields("id")
|
||||||
|
.execute()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package space.luminic.finance.services
|
||||||
|
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import space.luminic.finance.repos.SpaceRepo
|
||||||
|
import java.time.LocalDate
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class MonthlyExportScheduler(
|
||||||
|
private val spaceRepo: SpaceRepo,
|
||||||
|
private val transactionService: TransactionService,
|
||||||
|
private val googleDriveService: GoogleDriveService
|
||||||
|
) {
|
||||||
|
private val logger = LoggerFactory.getLogger(javaClass)
|
||||||
|
|
||||||
|
// Каждое 9 число в 23:59
|
||||||
|
@Scheduled(cron = "0 59 23 9 * ?")
|
||||||
|
fun exportMonthlyTransactions() {
|
||||||
|
logger.info("Starting monthly transaction export to Google Drive")
|
||||||
|
|
||||||
|
val spaces = spaceRepo.findAll()
|
||||||
|
val endDate = LocalDate.now() // 9th of current month
|
||||||
|
val startDate = endDate.minusMonths(1).plusDays(1) // 10th of previous month
|
||||||
|
|
||||||
|
val filter = TransactionService.TransactionsFilter(
|
||||||
|
dateFrom = startDate,
|
||||||
|
dateTo = endDate
|
||||||
|
)
|
||||||
|
|
||||||
|
for (space in spaces) {
|
||||||
|
val owner = space.owner
|
||||||
|
val refreshToken = owner.googleRefreshToken
|
||||||
|
|
||||||
|
if (refreshToken != null && refreshToken.isNotEmpty()) {
|
||||||
|
logger.info("Exporting for space \${space.id} (\${space.name}), owner \${owner.username}")
|
||||||
|
try {
|
||||||
|
val excelBytes = transactionService.generateExcel(space.id!!, filter)
|
||||||
|
val fileName = "Выгрузка_\${space.name}_\${startDate}_\${endDate}.xlsx"
|
||||||
|
googleDriveService.uploadExcelFile(refreshToken, fileName, excelBytes)
|
||||||
|
logger.info("Successfully exported to Google Drive for space \${space.id}")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger.error("Failed to export space \${space.id} to Google Drive", e)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.debug("Skipping space \${space.id} because owner hasn't linked Google Drive")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
src/main/resources/db/migration/V36__.sql
Normal file
1
src/main/resources/db/migration/V36__.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE finance.users ADD COLUMN google_refresh_token VARCHAR(255);
|
||||||
Reference in New Issue
Block a user