/* * Copyright (c) 2023 AtLarge Research * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ package org.opendc.web.server.model; import io.quarkus.hibernate.orm.panache.PanacheEntityBase; import io.quarkus.panache.common.Parameters; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Id; import jakarta.persistence.NamedQueries; import jakarta.persistence.NamedQuery; import jakarta.persistence.Table; import java.time.LocalDate; /** * Entity to track the number of simulation minutes used by a user. */ @Entity @Table @NamedQueries({ @NamedQuery( name = "UserAccounting.consumeBudget", query = """ UPDATE UserAccounting a SET a.simulationTime = a.simulationTime + :seconds WHERE a.userId = :userId AND a.periodEnd = :periodEnd """), @NamedQuery( name = "UserAccounting.resetBudget", query = """ UPDATE UserAccounting a SET a.periodEnd = :periodEnd, a.simulationTime = :seconds WHERE a.userId = :userId AND a.periodEnd = :oldPeriodEnd """) }) public class UserAccounting extends PanacheEntityBase { /** * User to which this object belongs. */ @Id @Column(name = "user_id", nullable = false) public String userId; /** * The end of the accounting period. */ @Column(name = "period_end", nullable = false) public LocalDate periodEnd; /** * The number of simulation seconds to be used per accounting period. */ @Column(name = "simulation_time_budget", nullable = false) public int simulationTimeBudget; /** * The number of simulation seconds used in this period. This number should reset once the accounting period has * been reached. */ @Column(name = "simulation_time", nullable = false) public int simulationTime = 0; /** * Construct a new {@link UserAccounting} object. * * @param userId The identifier of the user that this object belongs to. * @param periodEnd The end of the accounting period. * @param simulationTimeBudget The number of simulation seconds available per accounting period. */ public UserAccounting(String userId, LocalDate periodEnd, int simulationTimeBudget) { this.userId = userId; this.periodEnd = periodEnd; this.simulationTimeBudget = simulationTimeBudget; } /** * JPA constructor. */ protected UserAccounting() {} /** * Return the {@link UserAccounting} object associated with the specified user id. */ public static UserAccounting findByUser(String userId) { return findById(userId); } /** * Create a new {@link UserAccounting} object and persist it to the database. * * @param userId The identifier of the user that this object belongs to. * @param periodEnd The end of the accounting period. * @param simulationTimeBudget The number of simulation seconds available per accounting period. * @param simulationTime The initial simulation time that has been consumed. */ public static UserAccounting create( String userId, LocalDate periodEnd, int simulationTimeBudget, int simulationTime) { UserAccounting newAccounting = new UserAccounting(userId, periodEnd, simulationTimeBudget); newAccounting.simulationTime = simulationTime; newAccounting.persistAndFlush(); return newAccounting; } /** * Atomically consume the budget for this {@link UserAccounting} object. * * @param seconds The number of seconds to consume from the user. * @return true when the update succeeded, false when there was a conflict. */ public boolean consumeBudget(int seconds) { long count = update( "#UserAccounting.consumeBudget", Parameters.with("userId", userId).and("periodEnd", periodEnd).and("seconds", seconds)); return count > 0; } /** * Atomically reset the budget for this {@link UserAccounting} object. * * @param periodEnd The new end period for the budget. * @param seconds The number of seconds that have already been consumed. * @return true when the update succeeded`, false when there was a conflict. */ public boolean resetBudget(LocalDate periodEnd, int seconds) { long count = update( "#UserAccounting.resetBudget", Parameters.with("userId", userId) .and("oldPeriodEnd", this.periodEnd) .and("periodEnd", periodEnd) .and("seconds", seconds)); return count > 0; } /** * Determine whether the user has any remaining simulation budget. * * @return true when the user still has budget left, false otherwise. */ public boolean hasSimulationBudget() { var today = LocalDate.now(); // The accounting period must be over or there must be budget remaining. return !today.isBefore(periodEnd) || simulationTimeBudget > simulationTime; } }