/ contracts / src / deprecated / KaraokeSchoolV3.sol
KaraokeSchoolV3.sol
  1  // SPDX-License-Identifier: MIT
  2  pragma solidity ^0.8.20;
  3  
  4  import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
  5  import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
  6  import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
  7  
  8  interface IERC20 {
  9      function transfer(address to, uint256 amount) external returns (bool);
 10      function transferFrom(address from, address to, uint256 amount) external returns (bool);
 11      function balanceOf(address account) external view returns (uint256);
 12  }
 13  
 14  contract KaraokeSchoolV3 is Initializable, OwnableUpgradeable, UUPSUpgradeable {
 15      IERC20 public immutable usdcToken;
 16      address public immutable splitsContract;
 17      
 18      uint256 public constant COMBO_PRICE = 7_000_000; // 7 USDC (6 decimals)
 19      uint256 public constant VOICE_PACK_PRICE = 4_000_000; // 4 USDC
 20      uint256 public constant SONG_PACK_PRICE = 3_000_000; // 3 USDC
 21      
 22      // FIXED CREDIT AMOUNTS
 23      uint256 public constant COMBO_VOICE_CREDITS = 2000;
 24      uint256 public constant COMBO_SONG_CREDITS = 3;
 25      uint256 public constant VOICE_PACK_CREDITS = 2000;
 26      uint256 public constant SONG_PACK_CREDITS = 3;
 27      
 28      mapping(address => uint256) public voiceCredits;
 29      mapping(address => uint256) public songCredits;
 30      mapping(address => mapping(uint256 => bool)) public hasUnlockedSong;
 31      mapping(address => string) public userCountry;
 32      
 33      event CreditsPurchased(address indexed user, uint256 voiceAmount, uint256 songAmount);
 34      event SongUnlocked(address indexed user, uint256 indexed songId);
 35      event KaraokeStarted(address indexed user, uint256 indexed songId);
 36      event ExerciseStarted(address indexed user, uint256 exerciseCount);
 37      event PurchaseWithCountry(address indexed user, string country, uint256 usdcAmount, string packType);
 38      
 39      error InsufficientCredits();
 40      error AlreadyUnlocked();
 41      error SongNotUnlocked();
 42      error Unauthorized();
 43      error InvalidCountryCode();
 44      
 45      /// @custom:oz-upgrades-unsafe-allow constructor
 46      constructor(
 47          address _usdcToken, 
 48          address _splitsContract
 49      ) {
 50          usdcToken = IERC20(_usdcToken);
 51          splitsContract = _splitsContract;
 52          _disableInitializers();
 53      }
 54  
 55      function initialize(address initialOwner) external initializer {
 56          __Ownable_init(initialOwner);
 57          __UUPSUpgradeable_init();
 58      }
 59      
 60      // Required by UUPSUpgradeable - only owner can upgrade
 61      function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
 62      
 63      function buyCombopack(string calldata country) external {
 64          if (bytes(country).length != 2) revert InvalidCountryCode();
 65          
 66          // Transfer directly to splits contract
 67          require(usdcToken.transferFrom(msg.sender, splitsContract, COMBO_PRICE), "Transfer failed");
 68          
 69          // Store country if first purchase
 70          if (bytes(userCountry[msg.sender]).length == 0) {
 71              userCountry[msg.sender] = country;
 72          }
 73          
 74          // FIXED: Correct credit amounts
 75          voiceCredits[msg.sender] += COMBO_VOICE_CREDITS;
 76          songCredits[msg.sender] += COMBO_SONG_CREDITS;
 77          
 78          emit CreditsPurchased(msg.sender, COMBO_VOICE_CREDITS, COMBO_SONG_CREDITS);
 79          emit PurchaseWithCountry(msg.sender, country, COMBO_PRICE, "combo");
 80      }
 81      
 82      function buyVoicePack(string calldata country) external {
 83          if (bytes(country).length != 2) revert InvalidCountryCode();
 84          
 85          require(usdcToken.transferFrom(msg.sender, splitsContract, VOICE_PACK_PRICE), "Transfer failed");
 86          
 87          if (bytes(userCountry[msg.sender]).length == 0) {
 88              userCountry[msg.sender] = country;
 89          }
 90          
 91          voiceCredits[msg.sender] += VOICE_PACK_CREDITS;
 92          
 93          emit CreditsPurchased(msg.sender, VOICE_PACK_CREDITS, 0);
 94          emit PurchaseWithCountry(msg.sender, country, VOICE_PACK_PRICE, "voice");
 95      }
 96      
 97      function buySongPack(string calldata country) external {
 98          if (bytes(country).length != 2) revert InvalidCountryCode();
 99          
100          require(usdcToken.transferFrom(msg.sender, splitsContract, SONG_PACK_PRICE), "Transfer failed");
101          
102          if (bytes(userCountry[msg.sender]).length == 0) {
103              userCountry[msg.sender] = country;
104          }
105          
106          songCredits[msg.sender] += SONG_PACK_CREDITS;
107          
108          emit CreditsPurchased(msg.sender, 0, SONG_PACK_CREDITS);
109          emit PurchaseWithCountry(msg.sender, country, SONG_PACK_PRICE, "song");
110      }
111      
112      function unlockSong(uint256 songId) external {
113          if (songCredits[msg.sender] == 0) revert InsufficientCredits();
114          if (hasUnlockedSong[msg.sender][songId]) revert AlreadyUnlocked();
115          
116          songCredits[msg.sender] -= 1;
117          hasUnlockedSong[msg.sender][songId] = true;
118          
119          emit SongUnlocked(msg.sender, songId);
120      }
121      
122      function startKaraoke(uint256 songId) external {
123          if (voiceCredits[msg.sender] < 30) revert InsufficientCredits();
124          if (!hasUnlockedSong[msg.sender][songId]) revert SongNotUnlocked();
125          
126          voiceCredits[msg.sender] -= 30;
127          
128          emit KaraokeStarted(msg.sender, songId);
129      }
130      
131      function startExercise(uint256 numExercises) external {
132          if (numExercises == 0) revert InsufficientCredits();
133          if (voiceCredits[msg.sender] < numExercises) revert InsufficientCredits();
134          
135          voiceCredits[msg.sender] -= numExercises;
136          
137          emit ExerciseStarted(msg.sender, numExercises);
138      }
139      
140      // Emergency function to recover any stuck tokens (not USDC from purchases)
141      function recoverToken(address token, uint256 amount) external onlyOwner {
142          require(token != address(usdcToken) || IERC20(token).balanceOf(address(this)) == 0, "Cannot withdraw purchase funds");
143          IERC20(token).transfer(owner(), amount);
144      }
145      
146      // Storage gap for future upgrades
147      uint256[50] private __gap;
148  }